1 # SPDX-FileCopyrightText: 2011-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """Creating offset polygons inside faces."""
7 __author__
= "howard.trickey@gmail.com"
12 from .triquad
import Sub2
, Add2
, Angle
, Ccw
, Normalized2
, Perp2
, Length2
, \
14 from .geom
import Points
20 """A Spoke is a line growing from an outer vertex to an inner one.
22 A Spoke is contained in an Offset (see below).
25 origin: int - index of origin point in a Points
26 dest: int - index of dest point
27 is_reflex: bool - True if spoke grows from a reflex angle
28 dir: (float, float, float) - direction vector (normalized)
29 speed: float - at time t, other end of spoke is
30 origin + t*dir. Speed is such that the wavefront
31 from the face edges moves at speed 1.
32 face: int - index of face containing this Spoke, in Offset
33 index: int - index of this Spoke in its face
34 destindex: int - index of Spoke dest in its face
37 def __init__(self
, v
, prev
, next
, face
, index
, points
):
38 """Set attribute of spoke from points making up initial angle.
40 The spoke grows from an angle inside a face along the bisector
41 of that angle. Its speed is 1/sin(.5a), where a is the angle
42 formed by (prev, v, next). That speed means that the perpendicular
43 from the end of the spoke to either of the prev->v or v->prev
44 edges will grow at speed 1.
47 v: int - index of point spoke grows from
48 prev: int - index of point before v on boundary (in CCW order)
49 next: int - index of point after v on boundary (in CCW order)
50 face: int - index of face containing this spoke, in containing offset
51 index: int - index of this spoke in its face
52 points: geom.Points - maps vertex indices to 3d coords
64 uin
= Normalized2(Sub2(vp
, prevp
))
65 uout
= Normalized2(Sub2(nextp
, vp
))
66 uavg
= Normalized2((0.5 * (uin
[0] + uout
[0]), \
67 0.5 * (uin
[1] + uout
[1])))
68 if abs(Length2(uavg
)) < TOL
:
69 # in and out vectors are reverse of each other
70 self
.dir = (uout
[0], uout
[1], 0.0)
71 self
.is_reflex
= False
74 # bisector direction is 90 degree CCW rotation of
75 # average incoming/outgoing
76 self
.dir = (-uavg
[1], uavg
[0], 0.0)
77 self
.is_reflex
= Ccw(next
, v
, prev
, points
)
78 ang
= Angle(prev
, v
, next
, points
) # in range [0, 180)
79 sin_half_ang
= math
.sin(math
.pi
* ang
/ 360.0)
80 if abs(sin_half_ang
) < TOL
:
83 self
.speed
= 1.0 / sin_half_ang
86 """Printing representation of a Spoke."""
88 return "@%d+%gt%s <%d,%d>" % (self
.origin
, \
89 self
.speed
, str(self
.dir), \
90 self
.face
, self
.index
)
92 def EndPoint(self
, t
, points
, vspeed
):
93 """Return the coordinates of the non-origin point at time t.
96 t: float - time to end of spoke
97 points: geom.Points - coordinate map
98 vspeed: float - speed in z direction
100 (float, float, float) - coords of spoke's endpoint at time t
103 p
= points
.pos
[self
.origin
]
106 return (p
[0] + v
* t
* d
[0], p
[1] + v
* t
* d
[1], p
[2] + vspeed
* t
)
108 def VertexEvent(self
, other
, points
):
109 """Intersect self with other spoke, and return the OffsetEvent, if any.
111 A vertex event is with one advancing spoke intersects an adjacent
112 adavancing spoke, forming a new vertex.
115 other: Spoke - other spoke to intersect with
118 None or OffsetEvent - if there's an intersection in the growing
119 directions of the spokes, will return the OffsetEvent for
121 if lines are collinear or parallel, return None
125 a
= vmap
[self
.origin
]
126 b
= Add2(a
, self
.dir)
127 c
= vmap
[other
.origin
]
128 d
= Add2(c
, other
.dir)
129 # find intersection of line ab with line cd
135 # lines or neither parallel nor collinear
136 si
= Perp2(v
, w
) / pp
137 ti
= Perp2(u
, w
) / pp
138 if si
>= 0 and ti
>= 0:
139 p
= LinInterp2(a
, b
, si
)
140 dist_ab
= si
* Length2(u
)
141 dist_cd
= ti
* Length2(v
)
142 time_ab
= dist_ab
/ self
.speed
143 time_cd
= dist_cd
/ other
.speed
144 time
= max(time_ab
, time_cd
)
145 return OffsetEvent(True, time
, p
, self
, other
)
148 def EdgeEvent(self
, other
, offset
):
149 """Intersect self with advancing edge and return OffsetEvent, if any.
151 An edge event is when one advancing spoke intersects an advancing
152 edge. Advancing edges start out as face edges and move perpendicular
153 to them, at a rate of 1. The endpoints of the edge are the advancing
154 spokes on either end of the edge (so the edge shrinks or grows as
155 it advances). At some time, the edge may shrink to nothing and there
156 will be no EdgeEvent after that time.
158 We represent an advancing edge by the first spoke (in CCW order
159 of face) of the pair of defining spokes.
161 At time t, end of this spoke is at
163 where o=self.origin, d=self.dir, s= self.speed.
164 The advancing edge line has this equation:
166 where oo, od, os are o, d, s for other spoke, and p is direction
167 vector parallel to advancing edge, and a is a real parameter.
168 Equating x and y of intersection point:
170 o.x + d.x*s*t = oo.x + od.x*os*t + p.x*w
171 o.y + d.y*s*t = oo.y + od.y*os*t + p.y*w
173 which can be rearranged into the form
181 other: Spoke - the edge out of this spoke's origin is the advancing
182 edge to be checked for intersection
183 offset: Offset - the containing Offset
185 None or OffsetEvent - with data about the intersection, if any
188 vmap
= offset
.polyarea
.points
.pos
189 o
= vmap
[self
.origin
]
190 oo
= vmap
[other
.origin
]
191 otherface
= offset
.facespokes
[other
.face
]
192 othernext
= otherface
[(other
.index
+ 1) % len(otherface
)]
193 oonext
= vmap
[othernext
.origin
]
194 p
= Normalized2(Sub2(oonext
, oo
))
197 b
= other
.dir[0] * other
.speed
- self
.dir[0] * self
.speed
198 e
= other
.dir[1] * other
.speed
- self
.dir[1] * self
.speed
204 t
= (d
- f
* a
/ c
) / dem
211 t
= (a
- c
* d
/ f
) / dem
218 # intersection is in backward direction along self spoke
221 # intersection on wrong side of first end of advancing line segment
223 # calculate the equivalent of w for the other end
224 aa
= o
[0] - oonext
[0]
225 dd
= o
[1] - oonext
[1]
226 bb
= othernext
.dir[0] * othernext
.speed
- self
.dir[0] * self
.speed
227 ee
= othernext
.dir[1] * othernext
.speed
- self
.dir[1] * self
.speed
231 ww
= (aa
- bb
* t
) / cc
233 ww
= (dd
- ee
* t
) / ff
238 evertex
= (o
[0] + self
.dir[0] * self
.speed
* t
, \
239 o
[1] + self
.dir[1] * self
.speed
* t
)
240 return OffsetEvent(False, t
, evertex
, self
, other
)
243 class OffsetEvent(object):
244 """An event involving a spoke during offset computation.
246 The events kinds are:
247 vertex event: the spoke intersects an adjacent spoke and makes a new
249 edge event: the spoke hits an advancing edge and splits it
252 is_vertex_event: True if this is a vertex event (else it is edge event)
253 time: float - time at which it happens (edges advance at speed 1)
254 event_vertex: (float, float) - intersection point of event
255 spoke: Spoke - the spoke that this event is for
256 other: Spoke - other spoke involved in event; if vertex event, this will
257 be an adjacent spoke that intersects; if an edge event, this is the
258 spoke whose origin's outgoing edge grows to hit this event's spoke
261 def __init__(self
, isv
, time
, evertex
, spoke
, other
):
262 """Creates and initializes attributes of an OffsetEvent."""
264 self
.is_vertex_event
= isv
266 self
.event_vertex
= evertex
271 """Printing representation of an event."""
273 if self
.is_vertex_event
:
277 return "%s t=%5f %s %s %s" % (c
, self
.time
, str(self
.event_vertex
), \
278 repr(self
.spoke
), repr(self
.other
))
281 class Offset(object):
282 """Represents an offset polygonal area, and used to construct one.
284 Currently, the polygonal area must lie approximately in the XY plane.
285 As well as growing inwards in that plane, the advancing lines also
286 move in the Z direction at the rate of vspeed.
289 polyarea: geom.PolyArea - the area we are offsetting from.
290 We share the polyarea.points, and add to it as points in
291 the offset polygonal area are computed.
292 facespokes: list of list of Spoke - each sublist is a closed face
293 (oriented CCW); the faces may mutually interfere.
294 These lists are spokes for polyarea.poly + polyarea.holes.
295 endtime: float - time when this offset hits its first
296 event (relative to beginning of this offset), or the amount
297 that takes this offset to the end of the total Build time
298 timesofar: float - sum of times taken by all containing Offsets
299 vspeed: float - speed that edges move perpendicular to offset plane
300 inneroffsets: list of Offset - the offsets that take over after this
304 def __init__(self
, polyarea
, time
, vspeed
):
305 """Set up initial state of Offset from a polyarea.
308 polyarea: geom.PolyArea
309 time: float - time so far
312 self
.polyarea
= polyarea
315 self
.timesofar
= time
317 self
.inneroffsets
= []
318 self
.InitFaceSpokes(polyarea
.poly
)
319 for f
in polyarea
.holes
:
320 self
.InitFaceSpokes(f
)
323 ans
= ["Offset: endtime=%g" % self
.endtime
]
324 for i
, face
in enumerate(self
.facespokes
):
325 ans
.append(("<%d>" % i
) + str([str(spoke
) for spoke
in face
]))
326 return '\n'.join(ans
)
328 def PrintNest(self
, indent_level
=0):
329 indent
= " " * indent_level
* 4
330 print(indent
+ "Offset timesofar=", self
.timesofar
, "endtime=",
332 print(indent
+ " polyarea=", self
.polyarea
.poly
, self
.polyarea
.holes
)
333 for o
in self
.inneroffsets
:
334 o
.PrintNest(indent_level
+ 1)
336 def InitFaceSpokes(self
, face_vertices
):
337 """Initialize the offset representation of a face from vertex list.
339 If the face has no area or too small an area, don't bother making it.
342 face_vertices: list of int - point indices for boundary of face
344 A new face (list of spokes) may be added to self.facespokes
347 n
= len(face_vertices
)
350 points
= self
.polyarea
.points
351 area
= abs(geom
.SignedArea(face_vertices
, points
))
354 findex
= len(self
.facespokes
)
355 fspokes
= [Spoke(v
, face_vertices
[(i
- 1) % n
], \
356 face_vertices
[(i
+ 1) % n
], findex
, i
, points
) \
357 for i
, v
in enumerate(face_vertices
)]
358 self
.facespokes
.append(fspokes
)
360 def NextSpokeEvents(self
, spoke
):
361 """Return the OffsetEvents that will next happen for a given spoke.
363 It might happen that some events happen essentially simultaneously,
364 and also it is convenient to separate Edge and Vertex events, so
366 But, for vertex events, only look at the event with the next Spoke,
367 as the event with the previous spoke will be accounted for when we
368 consider that previous spoke.
371 spoke: Spoke - a spoke in one of the faces of this object
373 (float, list of OffsetEvent, list of OffsetEvent) -
375 next Vertex event list and next Edge event list
378 facespokes
= self
.facespokes
[spoke
.face
]
383 # First find vertex event (only the one with next spoke)
384 next_spoke
= facespokes
[(spoke
.index
+ 1) % n
]
385 ev
= spoke
.VertexEvent(next_spoke
, self
.polyarea
.points
)
389 # Now find edge events, if this is a reflex vertex
391 prev_spoke
= facespokes
[(spoke
.index
- 1) % n
]
392 for f
in self
.facespokes
:
394 if other
== spoke
or other
== prev_spoke
:
396 ev
= spoke
.EdgeEvent(other
, self
)
398 if ev
.time
< bestt
- TOL
:
402 if abs(ev
.time
- bestt
) < TOL
:
404 return (bestt
, bestv
, beste
)
406 def Build(self
, target
=2e100
):
407 """Build the complete Offset structure or up until target time.
409 Find the next event(s), makes the appropriate inner Offsets
410 that are inside this one, and calls Build on those Offsets to continue
411 the process until only a single point is left or time reaches target.
416 for f
in self
.facespokes
:
418 (t
, ve
, ee
) = self
.NextSpokeEvents(s
)
422 if abs(t
- bestt
) < TOL
:
423 bestevs
[0].extend(ve
)
424 bestevs
[1].extend(ee
)
426 # could happen if polygon is oriented wrong
427 # or in other special cases
430 # seems to be in a loop, so quit
436 if target
< self
.endtime
:
437 self
.endtime
= target
438 newfaces
= self
.MakeNewFaces(self
.endtime
)
440 # Only vertex events.
441 # Merging of successive vertices in inset face will
442 # take care of the vertex events
443 newfaces
= self
.MakeNewFaces(self
.endtime
)
446 # First make the new faces (handles all vertex events)
447 newfaces
= self
.MakeNewFaces(self
.endtime
)
448 # Only do one edge event (handle other simultaneous edge
449 # events in subsequent recursive Build calls)
451 splitjoin
= self
.SplitJoinFaces(newfaces
, ee
[0])
452 nexttarget
= target
- self
.endtime
453 if len(newfaces
) > 0:
454 pa
= geom
.PolyArea(points
=self
.polyarea
.points
)
455 pa
.data
= self
.polyarea
.data
456 newt
= self
.timesofar
+ self
.endtime
457 pa2
= None # may make another
459 pa
.poly
= newfaces
[0]
460 pa
.holes
= newfaces
[1:]
461 elif splitjoin
[0] == 'split':
462 (_
, findex
, newface0
, newface1
) = splitjoin
464 # Outer poly of polyarea was split.
465 # Now there will be two polyareas.
466 # If there were holes, need to allocate according to
467 # which one contains the holes.
469 pa2
= geom
.PolyArea(points
=self
.polyarea
.points
)
470 pa2
.data
= self
.polyarea
.data
472 if len(newfaces
) > 1:
473 # print("need to allocate holes")
474 for hf
in newfaces
[1:]:
475 if pa
.ContainsPoly(hf
, self
.polyarea
.points
):
476 # print("add", hf, "to", pa.poly)
478 elif pa2
.ContainsPoly(hf
, self
.polyarea
.points
):
479 # print("add", hf, "to", pa2.poly)
482 print("whoops, hole in neither poly!")
483 self
.inneroffsets
= [Offset(pa
, newt
, self
.vspeed
), \
484 Offset(pa2
, newt
, self
.vspeed
)]
486 # A hole was split. New faces just replace the split one.
487 pa
.poly
= newfaces
[0]
488 pa
.holes
= newfaces
[0:findex
] + [newface0
, newface1
] + \
489 newfaces
[findex
+ 1:]
492 (_
, findex
, othfindex
, newface0
) = splitjoin
493 if findex
== 0 or othfindex
== 0:
494 # Outer poly was joined to one hole.
496 pa
.holes
= [f
for f
in newfaces
if f
is not None]
498 # Two holes were joined
499 pa
.poly
= newfaces
[0]
500 pa
.holes
= [f
for f
in newfaces
if f
is not None] + \
502 self
.inneroffsets
= [Offset(pa
, newt
, self
.vspeed
)]
504 self
.inneroffsets
.append(Offset(pa2
, newt
, self
.vspeed
))
506 for o
in self
.inneroffsets
:
509 def FaceAtSpokeEnds(self
, f
, t
):
510 """Return a new face that is at the spoke ends of face f at time t.
512 Also merges any adjacent approximately equal vertices into one vertex,
513 so returned list may be smaller than len(f).
514 Also sets the destindex fields of the spokes to the vertex they
518 f: list of Spoke - one of self.faces
519 t: float - time in this offset
521 list of int - indices into self.polyarea.points
522 (which has been extended with new ones)
526 points
= self
.polyarea
.points
527 for i
in range(0, len(f
)):
529 vcoords
= s
.EndPoint(t
, points
, self
.vspeed
)
530 v
= points
.AddPoint(vcoords
)
533 s
.destindex
= len(newface
) - 1
534 elif i
== len(f
) - 1 and v
== newface
[0]:
538 s
.destindex
= len(newface
) - 1
545 def MakeNewFaces(self
, t
):
546 """For each face in this offset, make new face extending spokes
552 list of list of int - list of new faces
556 for f
in self
.facespokes
:
557 newf
= self
.FaceAtSpokeEnds(f
, t
)
562 def SplitJoinFaces(self
, newfaces
, ev
):
563 r
"""Use event ev to split or join faces.
565 Given ev, an edge event, use the ev spoke to split the
566 other spoke's inner edge.
567 If the ev spoke's face and other's face are the same, this splits the
568 face into two; if the faces are different, it joins them into one.
569 We have just made faces at the end of the spokes.
570 We have to remove the edge going from the other spoke to its
571 next spoke, and replace it with two edges, going to and from
572 the event spoke's destination.
581 where sd is the event spoke and of is the "other spoke",
582 hg is a spoke, and cf, fg. ge, ad, and db are edges in
584 What we are to do is to split fg into two edges, with the
585 joining point attached where b,s,a join.
586 There are a bunch of special cases:
587 - one of split fg edges might have zero length because end points
588 are already coincident or nearly coincident.
592 newfaces: list of list of int - the new faces
593 ev: OffsetEvent - an edge event
595 faces in newfaces that are involved in split or join are
598 ('split', int, list of int, list of int) - int is the index in
599 newfaces of the face that was split, two lists are the
601 ('join', int, int, list of int) - two ints are the indices in
602 newfaces of the faces that were joined, and the list is
606 # print("SplitJoinFaces", newfaces, ev)
610 othfindex
= other
.face
611 newface
= newfaces
[findex
]
612 othface
= newfaces
[othfindex
]
622 # print("newface=", newface)
623 # if findex != othfindex: print("othface=", othface)
624 # print("d=", d, "f=", f, "c=", c, "g=", g, "e=", e, "a=", a, "b=", b)
627 # The two new faces put spoke si's dest on edge between
628 # pi's dest and qi (edge after pi)'s dest in original face.
629 # These are indices in the original face; the current dest face
630 # may have fewer elements because of merging successive points
631 if findex
== othfindex
:
632 # Case where splitting one new face into two.
633 # The new new faces are:
634 # [d, g, e, ..., a] and [d, b, ..., c, f]
635 # (except we actually want the vertex numbers at those positions)
636 newface0
= [newface
[d
]]
639 newface0
.append(newface
[i
])
641 newface1
= [newface
[d
]]
644 newface1
.append(newface
[i
])
646 newface1
.append(newface
[f
])
647 # print("newface0=", newface0, "newface1=", newface1)
648 # now the destindex values for the spokes are messed up
649 # but I don't think we need them again
650 newfaces
[findex
] = None
651 return ('split', findex
, newface0
, newface1
)
653 # Case where joining two faces into one.
654 # The new face is splicing d's face between
655 # f and g in other face (or the reverse of all of that).
656 newface0
= [othface
[i
] for i
in range(0, f
+ 1)]
657 newface0
.append(newface
[d
])
660 newface0
.append(newface
[i
])
662 newface0
.append(newface
[d
])
664 newface0
.extend([othface
[i
] for i
in range(g
, nonf
)])
665 # print("newface0=", newface0)
666 newfaces
[findex
] = None
667 newfaces
[othfindex
] = None
668 return ('join', findex
, othfindex
, newface0
)
670 def InnerPolyAreas(self
):
671 """Return the interior of the offset (and contained offsets) as
678 ans
= geom
.PolyAreas()
679 ans
.points
= self
.polyarea
.points
680 _AddInnerAreas(self
, ans
)
684 """Returns the maximum offset amount possible.
686 float - maximum amount
689 # Need to do Build on a copy of points
690 # so don't add points that won't be used when
691 # really do a Build with a smaller amount
692 test_points
= geom
.Points()
693 test_points
.AddPoints(self
.polyarea
.points
, True)
694 save_points
= self
.polyarea
.points
695 self
.polyarea
.points
= test_points
697 max_amount
= self
._MaxTime
()
698 self
.polyarea
.points
= save_points
702 if self
.inneroffsets
:
703 return max([o
._MaxTime
() for o
in self
.inneroffsets
])
705 return self
.timesofar
+ self
.endtime
708 def _AddInnerAreas(off
, polyareas
):
709 """Add the innermost areas of offset off to polyareas.
711 Assume that polyareas is already using the proper shared points.
715 polyareas: geom.PolyAreas
717 Any non-zero-area faces in the very inside of off are
722 for o
in off
.inneroffsets
:
723 _AddInnerAreas(o
, polyareas
)
725 newpa
= geom
.PolyArea(polyareas
.points
)
726 for i
, f
in enumerate(off
.facespokes
):
727 newface
= off
.FaceAtSpokeEnds(f
, off
.endtime
)
728 area
= abs(geom
.SignedArea(newface
, polyareas
.points
))
736 newpa
.data
= off
.polyarea
.data
738 newpa
.holes
.append(newface
)
740 polyareas
.polyareas
.append(newpa
)