diff options
author | Howard Trickey <howard.trickey@gmail.com> | 2011-05-18 18:15:18 +0400 |
---|---|---|
committer | Howard Trickey <howard.trickey@gmail.com> | 2011-05-18 18:15:18 +0400 |
commit | df326083828858b4489fb31c1b8bcbb63fce5a14 (patch) | |
tree | 87021ac68019130a918934a3f854e42f10d2a7bf /mesh_inset/offset.py | |
parent | ec7aaa4402261950d803cd69e4ecdd33c51cd56a (diff) |
Added percent option; preserve material and smoothing
Diffstat (limited to 'mesh_inset/offset.py')
-rw-r--r-- | mesh_inset/offset.py | 1364 |
1 files changed, 699 insertions, 665 deletions
diff --git a/mesh_inset/offset.py b/mesh_inset/offset.py index 7c199469..3a44b95b 100644 --- a/mesh_inset/offset.py +++ b/mesh_inset/offset.py @@ -16,6 +16,8 @@ # # ##### END GPL LICENSE BLOCK ##### +# <pep8 compliant> + """Creating offset polygons inside faces.""" __author__ = "howard.trickey@gmail.com" @@ -29,693 +31,725 @@ from .geom import Points AREATOL = 1e-4 -class Spoke(object): - """A Spoke is a line growing from an outer vertex to an inner one. - - A Spoke is contained in an Offset (see below). - - Attributes: - origin: int - index of origin point in a Points - dest: int - index of dest point - is_reflex: bool - True if spoke grows from a reflex angle - dir: (float, float, float) - direction vector (normalized) - speed: float - at time t, other end of spoke is - origin + t*dir. Speed is such that the wavefront - from the face edges moves at speed 1. - face: int - index of face containing this Spoke, in Offset - index: int - index of this Spoke in its face - destindex: int - index of Spoke dest in its face - """ - - def __init__(self, v, prev, next, face, index, points): - """Set attribute of spoke from points making up initial angle. - - The spoke grows from an angle inside a face along the bisector - of that angle. Its speed is 1/sin(.5a), where a is the angle - formed by (prev, v, next). That speed means that the perpendicular - from the end of the spoke to either of the prev->v or v->prev - edges will grow at speed 1. - - Args: - v: int - index of point spoke grows from - prev: int - index of point before v on boundary (in CCW order) - next: int - index of point after v on boundary (in CCW order) - face: int - index of face containing this spoke, in containing offset - index: int - index of this spoke in its face - points: geom.Points - maps vertex indices to 3d coords - """ - self.origin = v - self.dest = v - self.face = face - self.index = index - self.destindex = -1 - vmap = points.pos - vp = vmap[v] - prevp = vmap[prev] - nextp = vmap[next] - uin = Normalized2(Sub2(vp, prevp)) - uout = Normalized2(Sub2(nextp, vp)) - uavg = Normalized2((0.5*(uin[0]+uout[0]), 0.5*(uin[1]+uout[1]))) - if abs(Length2(uavg)) < TOL: - # in and out vectors are reverse of each other - self.dir = (uout[0], uout[1], 0.0) - self.is_reflex = False - self.speed = 1e7 - else: - # bisector direction is 90 degree CCW rotation of average incoming/outgoing - self.dir = (-uavg[1], uavg[0], 0.0) - self.is_reflex = Ccw(next, v, prev, points) - ang = Angle(prev, v, next, points) # in range [0, 180) - sin_half_ang = math.sin(math.pi*ang / 360.0) - if abs(sin_half_ang) < TOL: - self.speed = 1e7 - else: - self.speed = 1.0 / sin_half_ang - - def __repr__(self): - """Printing representation of a Spoke.""" - - return "@%d+%gt%s <%d,%d>" % (self.origin, \ - self.speed, str(self.dir), \ - self.face, self.index) - - def EndPoint(self, t, points, vspeed): - """Return the coordinates of the non-origin point at time t. - - Args: - t: float - time to end of spoke - points: geom.Points - coordinate map - vspeed: float - speed in z direction - Returns: - (float, float, float) - coords of spoke's endpoint at time t +class Spoke(object): + """A Spoke is a line growing from an outer vertex to an inner one. + + A Spoke is contained in an Offset (see below). + + Attributes: + origin: int - index of origin point in a Points + dest: int - index of dest point + is_reflex: bool - True if spoke grows from a reflex angle + dir: (float, float, float) - direction vector (normalized) + speed: float - at time t, other end of spoke is + origin + t*dir. Speed is such that the wavefront + from the face edges moves at speed 1. + face: int - index of face containing this Spoke, in Offset + index: int - index of this Spoke in its face + destindex: int - index of Spoke dest in its face """ - p = points.pos[self.origin] - d = self.dir - v = self.speed - return (p[0] + v*t*d[0], p[1] + v*t*d[1], p[2] + vspeed*t) - + def __init__(self, v, prev, next, face, index, points): + """Set attribute of spoke from points making up initial angle. + + The spoke grows from an angle inside a face along the bisector + of that angle. Its speed is 1/sin(.5a), where a is the angle + formed by (prev, v, next). That speed means that the perpendicular + from the end of the spoke to either of the prev->v or v->prev + edges will grow at speed 1. + + Args: + v: int - index of point spoke grows from + prev: int - index of point before v on boundary (in CCW order) + next: int - index of point after v on boundary (in CCW order) + face: int - index of face containing this spoke, in containing offset + index: int - index of this spoke in its face + points: geom.Points - maps vertex indices to 3d coords + """ + + self.origin = v + self.dest = v + self.face = face + self.index = index + self.destindex = -1 + vmap = points.pos + vp = vmap[v] + prevp = vmap[prev] + nextp = vmap[next] + uin = Normalized2(Sub2(vp, prevp)) + uout = Normalized2(Sub2(nextp, vp)) + uavg = Normalized2((0.5 * (uin[0] + uout[0]), \ + 0.5 * (uin[1] + uout[1]))) + if abs(Length2(uavg)) < TOL: + # in and out vectors are reverse of each other + self.dir = (uout[0], uout[1], 0.0) + self.is_reflex = False + self.speed = 1e7 + else: + # bisector direction is 90 degree CCW rotation of + # average incoming/outgoing + self.dir = (-uavg[1], uavg[0], 0.0) + self.is_reflex = Ccw(next, v, prev, points) + ang = Angle(prev, v, next, points) # in range [0, 180) + sin_half_ang = math.sin(math.pi * ang / 360.0) + if abs(sin_half_ang) < TOL: + self.speed = 1e7 + else: + self.speed = 1.0 / sin_half_ang + + def __repr__(self): + """Printing representation of a Spoke.""" + + return "@%d+%gt%s <%d,%d>" % (self.origin, \ + self.speed, str(self.dir), \ + self.face, self.index) + + def EndPoint(self, t, points, vspeed): + """Return the coordinates of the non-origin point at time t. + + Args: + t: float - time to end of spoke + points: geom.Points - coordinate map + vspeed: float - speed in z direction + Returns: + (float, float, float) - coords of spoke's endpoint at time t + """ + + p = points.pos[self.origin] + d = self.dir + v = self.speed + return (p[0] + v * t * d[0], p[1] + v * t * d[1], p[2] + vspeed * t) + + def VertexEvent(self, other, points): + """Intersect self with other spoke, and return the OffsetEvent, if any. + + A vertex event is with one advancing spoke intersects an adjacent + adavancing spoke, forming a new vertex. + + Args: + other: Spoke - other spoke to intersect with + points: Geom.points + Returns: + None or OffsetEvent - if there's an intersection in the growing + directions of the spokes, will return the OffsetEvent for + the intersection; + if lines are collinear or parallel, return None + """ + + vmap = points.pos + a = vmap[self.origin] + b = Add2(a, self.dir) + c = vmap[other.origin] + d = Add2(c, other.dir) + # find intersection of line ab with line cd + u = Sub2(b, a) + v = Sub2(d, c) + w = Sub2(a, c) + pp = Perp2(u, v) + if abs(pp) > TOL: + # lines or neither parallel nor collinear + si = Perp2(v, w) / pp + ti = Perp2(u, w) / pp + if si >= 0 and ti >= 0: + p = LinInterp2(a, b, si) + dist_ab = si * Length2(u) + dist_cd = ti * Length2(v) + time_ab = dist_ab / self.speed + time_cd = dist_cd / other.speed + time = max(time_ab, time_cd) + return OffsetEvent(True, time, p, self, other) + return None - def VertexEvent(self, other, points): - """Intersect self with other spoke, and return the OffsetEvent, if any. + def EdgeEvent(self, other, offset): + """Intersect self with advancing edge and return OffsetEvent, if any. + + An edge event is when one advancing spoke intersects an advancing + edge. Advancing edges start out as face edges and move perpendicular + to them, at a rate of 1. The endpoints of the edge are the advancing + spokes on either end of the edge (so the edge shrinks or grows as + it advances). At some time, the edge may shrink to nothing and there + will be no EdgeEvent after that time. + + We represent an advancing edge by the first spoke (in CCW order + of face) of the pair of defining spokes. + + At time t, end of this spoke is at + o + d*s*t + where o=self.origin, d=self.dir, s= self.speed. + The advancing edge line has this equation: + oo + od*os*t + p*a + where oo, od, os are o, d, s for other spoke, and p is direction + vector parallel to advancing edge, and a is a real parameter. + Equating x and y of intersection point: + + o.x + d.x*s*t = oo.x + od.x*os*t + p.x*w + o.y + d.y*s*t = oo.y + od.y*os*t + p.y*w + + which can be rearranged into the form + + a = bt + cw + d = et + fw + + and solved for t, w. + + Args: + other: Spoke - the edge out of this spoke's origin is the advancing + edge to be checked for intersection + offset: Offset - the containing Offset + Returns: + None or OffsetEvent - with data about the intersection, if any + """ + + vmap = offset.polyarea.points.pos + o = vmap[self.origin] + oo = vmap[other.origin] + otherface = offset.facespokes[other.face] + othernext = otherface[(other.index + 1) % len(otherface)] + oonext = vmap[othernext.origin] + p = Normalized2(Sub2(oonext, oo)) + a = o[0] - oo[0] + d = o[1] - oo[1] + b = other.dir[0] * other.speed - self.dir[0] * self.speed + e = other.dir[1] * other.speed - self.dir[1] * self.speed + c = p[0] + f = p[1] + if abs(c) > TOL: + dem = e - f * b / c + if abs(dem) > TOL: + t = (d - f * a / c) / dem + w = (a - b * t) / c + else: + return None + elif abs(f) > TOL: + dem = b - c * e / f + if abs(dem) > TOL: + t = (a - c * d / f) / dem + w = (d - e * t) / f + else: + return None + else: + return None + if t < 0.0: + # intersection is in backward direction along self spoke + return None + if w < 0.0: + # intersection on wrong side of first end of advancing line segment + return None + # calculate the equivalent of w for the other end + aa = o[0] - oonext[0] + dd = o[1] - oonext[1] + bb = othernext.dir[0] * othernext.speed - self.dir[0] * self.speed + ee = othernext.dir[1] * othernext.speed - self.dir[1] * self.speed + cc = -p[0] + ff = -p[1] + if abs(cc) > TOL: + ww = (aa - bb * t) / cc + elif abs(ff) > TOL: + ww = (dd - ee * t) / ff + else: + return None + if ww < 0.0: + return None + evertex = (o[0] + self.dir[0] * self.speed * t, \ + o[1] + self.dir[1] * self.speed * t) + return OffsetEvent(False, t, evertex, self, other) - A vertex event is with one advancing spoke intersects an adjacent - adavancing spoke, forming a new vertex. - Args: - other: Spoke - other spoke to intersect with - points: Geom.points - Returns: - None or OffsetEvent - if there's an intersection in the growing - directions of the spokes, will return the OffsetEvent for - the intersection; - if lines are collinear or parallel, return None - """ - - vmap = points.pos - a = vmap[self.origin] - b = Add2(a, self.dir) - c = vmap[other.origin] - d = Add2(c, other.dir) - # find intersection of line ab with line cd - u = Sub2(b, a) - v = Sub2(d, c) - w = Sub2(a, c) - pp = Perp2(u, v) - if abs(pp) > TOL: - # lines or neither parallel nor collinear - si = Perp2(v, w) / pp - ti = Perp2(u, w) / pp - if si >= 0 and ti >= 0: - p = LinInterp2(a, b, si) - dist_ab = si*Length2(u) - dist_cd = ti*Length2(v) - time_ab = dist_ab / self.speed - time_cd = dist_cd / other.speed - time = max(time_ab, time_cd) - return OffsetEvent(True, time, p, self, other) - return None - - def EdgeEvent(self, other, offset): - """Intersect self with advancing edge and return OffsetEvent, if any. - - An edge event is when one advancing spoke intersects an advancing - edge. Advancing edges start out as face edges and move perpendicular - to them, at a rate of 1. The endpoints of the edge are the advancing - spokes on either end of the edge (so the edge shrinks or grows as - it advances). At some time, the edge may shrink to nothing and there - will be no EdgeEvent after that time. - - We represent an advancing edge by the first spoke (in CCW order - of face) of the pair of defining spokes. - - At time t, end of this spoke is at - o + d*s*t - where o=self.origin, d=self.dir, s= self.speed. - The advancing edge line has this equation: - oo + od*os*t + p*a - where oo, od, os are o, d, s for other spoke, and p is direction - vector parallel to advancing edge, and a is a real parameter. - Equating x and y of intersection point: - - o.x + d.x*s*t = oo.x + od.x*os*t + p.x*w - o.y + d.y*s*t = oo.y + od.y*os*t + p.y*w - - which can be rearranged into the form - - a = bt + cw - d = et + fw - - and solved for t, w. - - Args: - other: Spoke - the edge out of this spoke's origin is the advancing - edge to be checked for intersection - offset: Offset - the containing Offset - Returns: - None or OffsetEvent - with data about the intersection, if any +class OffsetEvent(object): + """An event involving a spoke during offset computation. + + The events kinds are: + vertex event: the spoke intersects an adjacent spoke and makes a new + vertex + edge event: the spoke hits an advancing edge and splits it + + Attributes: + is_vertex_event: True if this is a vertex event (else it is edge event) + time: float - time at which it happens (edges advance at speed 1) + event_vertex: (float, float) - intersection point of event + spoke: Spoke - the spoke that this event is for + other: Spoke - other spoke involved in event; if vertex event, this will + be an adjacent spoke that intersects; if an edge event, this is the + spoke whose origin's outgoing edge grows to hit this event's spoke """ - vmap = offset.polyarea.points.pos - o = vmap[self.origin] - oo = vmap[other.origin] - otherface = offset.facespokes[other.face] - othernext = otherface[(other.index+1) % len(otherface)] - oonext = vmap[othernext.origin] - p = Normalized2(Sub2(oonext, oo)) - a = o[0] - oo[0] - d = o[1] - oo[1] - b = other.dir[0]*other.speed - self.dir[0]*self.speed - e = other.dir[1]*other.speed - self.dir[1]*self.speed - c = p[0] - f = p[1] - if abs(c) > TOL: - dem = e - f*b/c - if abs(dem) > TOL: - t = (d - f*a/c) / dem - w = (a - b*t) / c - else: - return None - elif abs(f) > TOL: - dem = b - c*e/f - if abs(dem) > TOL: - t = (a - c*d/f) / dem - w = (d - e*t) / f - else: - return None - else: - return None - if t < 0.0: - # intersection is in backward direction along self spoke - return None - if w < 0.0: - # intersection is on wrong side of first end of advancing line segment - return None - # calculate the equivalent of w for the other end - aa = o[0] - oonext[0] - dd = o[1] - oonext[1] - bb = othernext.dir[0]*othernext.speed - self.dir[0]*self.speed - ee = othernext.dir[1]*othernext.speed - self.dir[1]*self.speed - cc = -p[0] - ff = -p[1] - if abs(cc) > TOL: - ww = (aa - bb*t) / cc - elif abs(ff) > TOL: - ww = (dd - ee*t) / ff - else: - return None - if ww < 0.0: - return None - evertex = (o[0] + self.dir[0]*self.speed*t, \ - o[1] + self.dir[1]*self.speed*t) - return OffsetEvent(False, t, evertex, self, other) - + def __init__(self, isv, time, evertex, spoke, other): + """Creates and initializes attributes of an OffsetEvent.""" -class OffsetEvent(object): - """An event involving a spoke during offset computation. - - The events kinds are: - vertex event: the spoke intersects an adjacent spoke and makes a new vertex - edge event: the spoke hits an advancing edge and splits it - - Attributes: - is_vertex_event: True if this is a vertex event (else it is edge event) - time: float - time at which it happens (edges advance at speed 1) - event_vertex: (float, float) - intersection point of event - spoke: Spoke - the spoke that this event is for - other: Spoke - other spoke involved in event; if vertex event, this will - be an adjacent spoke that intersects; if an edge event, this is the - spoke whose origin's outgoing edge grows to hit this event's spoke - """ - - def __init__(self, isv, time, evertex, spoke, other): - """Creates and initializes attributes of an OffsetEvent.""" - - self.is_vertex_event = isv - self.time = time - self.event_vertex = evertex - self.spoke = spoke - self.other = other - - def __repr__(self): - """Printing representation of an event.""" - - if self.is_vertex_event: - c = "V" - else: - c = "E" - return "%s t=%5f %s %s %s" % (c, self.time, str(self.event_vertex), \ - repr(self.spoke), repr(self.other)) + self.is_vertex_event = isv + self.time = time + self.event_vertex = evertex + self.spoke = spoke + self.other = other + def __repr__(self): + """Printing representation of an event.""" -class Offset(object): - """Represents an offset polygonal area, and used to construct one. - - Currently, the polygonal area must lie approximately in the XY plane. - As well as growing inwards in that plane, the advancing lines also - move in the Z direction at the rate of vspeed. - - Attributes: - polyarea: geom.PolyArea - the area we are offsetting from. - We share the polyarea.points, and add to it as points in - the offset polygonal area are computed. - facespokes: list of list of Spoke - each sublist is a closed face - (oriented CCW); the faces may mutually interfere. - These lists are spokes for polyarea.poly + polyarea.holes. - endtime: float - time when this offset hits its first - event (relative to beginning of this offset), or the amount - that takes this offset to the end of the total Build time - timesofar: float - sum of times taken by all containing Offsets - vspeed: float - speed that edges move perpendicular to offset plane - inneroffsets: list of Offset - the offsets that take over after this (inside it) - """ - - def __init__(self, polyarea, time, vspeed): - """Set up initial state of Offset from a polyarea. - - Args: - polyarea: geom.PolyArea - time: float - time so far - """ + if self.is_vertex_event: + c = "V" + else: + c = "E" + return "%s t=%5f %s %s %s" % (c, self.time, str(self.event_vertex), \ + repr(self.spoke), repr(self.other)) - self.polyarea = polyarea - self.facespokes = [] - self.endtime = 0.0 - self.timesofar = time - self.vspeed = vspeed - self.inneroffsets = [] - self.InitFaceSpokes(polyarea.poly) - for f in polyarea.holes: - self.InitFaceSpokes(f) - - def __repr__(self): - ans = ["Offset: endtime=%g" % self.endtime] - for i, face in enumerate(self.facespokes): - ans.append(("<%d>" % i) + str([ str(spoke) for spoke in face ])) - return '\n'.join(ans) - - def PrintNest(self, indent_level=0): - indent = " " * indent_level * 4 - print(indent + "Offset timesofar=", self.timesofar, "endtime=", self.endtime) - print(indent + " polyarea=", self.polyarea.poly, self.polyarea.holes) - for o in self.inneroffsets: - o.PrintNest(indent_level+1) - - def InitFaceSpokes(self, face_vertices): - """Initialize the offset representation of a face from vertex list. - - If the face has no area or too small an area, don't bother making it. - - Args: - face_vertices: list of int - point indices for boundary of face - Side effect: - A new face (list of spokes) may be added to self.facespokes - """ - n = len(face_vertices) - if n <= 2: - return - points = self.polyarea.points - area = abs(geom.SignedArea(face_vertices, points)) - if area < AREATOL: - return - findex = len(self.facespokes) - fspokes = [ Spoke(v, face_vertices[(i-1) % n], \ - face_vertices[(i+1) % n], findex, i, points) \ - for i, v in enumerate(face_vertices) ] - self.facespokes.append(fspokes) - - def NextSpokeEvents(self, spoke): - """Return the OffsetEvents that will next happen for a given spoke. - - It might happen that some events happen essentially simultaneously, - and also it is convenient to separate Edge and Vertex events, so - we return two lists. - But, for vertex events, only look at the event with the next Spoke, - as the event with the previous spoke will be accounted for when we - consider that previous spoke. - - Args: - spoke: Spoke - a spoke in one of the faces of this object - Returns: - (float, list of OffsetEvent, list of OffsetEvent) - time of next event, - next Vertex event list and next Edge event list - """ - - facespokes = self.facespokes[spoke.face] - n = len(facespokes) - bestt = 1e100 - bestv = [] - beste = [] - # First find vertex event (only the one with next spoke) - next_spoke = facespokes[(spoke.index+1) % n] - ev = spoke.VertexEvent(next_spoke, self.polyarea.points) - if ev: - bestv = [ev] - bestt = ev.time - # Now find edge events, if this is a reflex vertex - if spoke.is_reflex: - prev_spoke = facespokes[(spoke.index-1) % n] - for f in self.facespokes: - for other in f: - if other == spoke or other == prev_spoke: - continue - ev = spoke.EdgeEvent(other, self) - if ev: - if ev.time < bestt - TOL: - beste = [] - bestv = [] - bestt = ev.time - if abs(ev.time - bestt) < TOL: - beste.append(ev) - return (bestt, bestv, beste) - - def Build(self, target = 2e100): - """Build the complete Offset structure or up until target time. - - Find the next event(s), makes the appropriate inner Offsets - that are inside this one, and calls Build on those Offsets to continue the - process until only a single point is left or time reaches target. +class Offset(object): + """Represents an offset polygonal area, and used to construct one. + + Currently, the polygonal area must lie approximately in the XY plane. + As well as growing inwards in that plane, the advancing lines also + move in the Z direction at the rate of vspeed. + + Attributes: + polyarea: geom.PolyArea - the area we are offsetting from. + We share the polyarea.points, and add to it as points in + the offset polygonal area are computed. + facespokes: list of list of Spoke - each sublist is a closed face + (oriented CCW); the faces may mutually interfere. + These lists are spokes for polyarea.poly + polyarea.holes. + endtime: float - time when this offset hits its first + event (relative to beginning of this offset), or the amount + that takes this offset to the end of the total Build time + timesofar: float - sum of times taken by all containing Offsets + vspeed: float - speed that edges move perpendicular to offset plane + inneroffsets: list of Offset - the offsets that take over after this + (inside it) """ - bestt = 1e100 - bestevs = [[], []] - for f in self.facespokes: - for s in f: - (t, ve, ee) = self.NextSpokeEvents(s) - if t < bestt - TOL: - bestevs = [[], []] - bestt = t - if abs(t-bestt) < TOL: - bestevs[0].extend(ve) - bestevs[1].extend(ee) - if bestt == 1e100: - # could happen if polygon is oriented wrong - # or in other special cases - return - if abs(bestt) < TOL: - # seems to be in a loop, so quit - return - self.endtime = bestt - (ve, ee) = bestevs - newfaces = [] - splitjoin = None - if target < self.endtime: - self.endtime = target - newfaces = self.MakeNewFaces(self.endtime) - elif ve and not ee: - # Only vertex events. - # Merging of successive vertices in inset face will - # take care of the vertex events - newfaces = self.MakeNewFaces(self.endtime) - else: - # Edge events too - # First make the new faces (handles all vertex events) - newfaces = self.MakeNewFaces(self.endtime) - # Only do one edge event (handle other simultaneous edge - # events in subsequent recursive Build calls) - splitjoin = self.SplitJoinFaces(newfaces, ee[0]) - nexttarget = target - self.endtime - if len(newfaces) > 0: - pa = geom.PolyArea(points = self.polyarea.points) - pa.color = self.polyarea.color - newt = self.timesofar+self.endtime - pa2 = None # may make another - if not splitjoin: - pa.poly = newfaces[0] - pa.holes = newfaces[1:] - elif splitjoin[0] == 'split': - (_, findex, newface0, newface1) = splitjoin - if findex == 0: - # Outer poly of polyarea was split. - # Now there will be two polyareas. - # If there were holes, need to allocate according to - # which one contains the holes. - pa.poly = newface0 - pa2 = geom.PolyArea(points = self.polyarea.points) - pa2.color = self.polyarea.color - pa2.poly = newface1 - if len(newfaces) > 1: - # print("need to allocate holes") - for hf in newfaces[1:]: - if pa.ContainsPoly(hf, self.polyarea.points): - # print("add", hf, "to", pa.poly) - pa.holes.append(hf) - elif pa2.ContainsPoly(hf, self.polyarea.points): - # print("add", hf, "to", pa2.poly) - pa2.holes.append(hf) - else: - print("whoops, hole in neither poly!") - self.inneroffsets = [ Offset(pa, newt, self.vspeed), Offset(pa2, newt, self.vspeed) ] + def __init__(self, polyarea, time, vspeed): + """Set up initial state of Offset from a polyarea. + + Args: + polyarea: geom.PolyArea + time: float - time so far + """ + + self.polyarea = polyarea + self.facespokes = [] + self.endtime = 0.0 + self.timesofar = time + self.vspeed = vspeed + self.inneroffsets = [] + self.InitFaceSpokes(polyarea.poly) + for f in polyarea.holes: + self.InitFaceSpokes(f) + + def __repr__(self): + ans = ["Offset: endtime=%g" % self.endtime] + for i, face in enumerate(self.facespokes): + ans.append(("<%d>" % i) + str([str(spoke) for spoke in face])) + return '\n'.join(ans) + + def PrintNest(self, indent_level=0): + indent = " " * indent_level * 4 + print(indent + "Offset timesofar=", self.timesofar, "endtime=", + self.endtime) + print(indent + " polyarea=", self.polyarea.poly, self.polyarea.holes) + for o in self.inneroffsets: + o.PrintNest(indent_level + 1) + + def InitFaceSpokes(self, face_vertices): + """Initialize the offset representation of a face from vertex list. + + If the face has no area or too small an area, don't bother making it. + + Args: + face_vertices: list of int - point indices for boundary of face + Side effect: + A new face (list of spokes) may be added to self.facespokes + """ + + n = len(face_vertices) + if n <= 2: + return + points = self.polyarea.points + area = abs(geom.SignedArea(face_vertices, points)) + if area < AREATOL: + return + findex = len(self.facespokes) + fspokes = [Spoke(v, face_vertices[(i - 1) % n], \ + face_vertices[(i + 1) % n], findex, i, points) \ + for i, v in enumerate(face_vertices)] + self.facespokes.append(fspokes) + + def NextSpokeEvents(self, spoke): + """Return the OffsetEvents that will next happen for a given spoke. + + It might happen that some events happen essentially simultaneously, + and also it is convenient to separate Edge and Vertex events, so + we return two lists. + But, for vertex events, only look at the event with the next Spoke, + as the event with the previous spoke will be accounted for when we + consider that previous spoke. + + Args: + spoke: Spoke - a spoke in one of the faces of this object + Returns: + (float, list of OffsetEvent, list of OffsetEvent) - + time of next event, + next Vertex event list and next Edge event list + """ + + facespokes = self.facespokes[spoke.face] + n = len(facespokes) + bestt = 1e100 + bestv = [] + beste = [] + # First find vertex event (only the one with next spoke) + next_spoke = facespokes[(spoke.index + 1) % n] + ev = spoke.VertexEvent(next_spoke, self.polyarea.points) + if ev: + bestv = [ev] + bestt = ev.time + # Now find edge events, if this is a reflex vertex + if spoke.is_reflex: + prev_spoke = facespokes[(spoke.index - 1) % n] + for f in self.facespokes: + for other in f: + if other == spoke or other == prev_spoke: + continue + ev = spoke.EdgeEvent(other, self) + if ev: + if ev.time < bestt - TOL: + beste = [] + bestv = [] + bestt = ev.time + if abs(ev.time - bestt) < TOL: + beste.append(ev) + return (bestt, bestv, beste) + + def Build(self, target=2e100): + """Build the complete Offset structure or up until target time. + + Find the next event(s), makes the appropriate inner Offsets + that are inside this one, and calls Build on those Offsets to continue + the process until only a single point is left or time reaches target. + """ + + bestt = 1e100 + bestevs = [[], []] + for f in self.facespokes: + for s in f: + (t, ve, ee) = self.NextSpokeEvents(s) + if t < bestt - TOL: + bestevs = [[], []] + bestt = t + if abs(t - bestt) < TOL: + bestevs[0].extend(ve) + bestevs[1].extend(ee) + if bestt == 1e100: + # could happen if polygon is oriented wrong + # or in other special cases + return + if abs(bestt) < TOL: + # seems to be in a loop, so quit + return + self.endtime = bestt + (ve, ee) = bestevs + newfaces = [] + splitjoin = None + if target < self.endtime: + self.endtime = target + newfaces = self.MakeNewFaces(self.endtime) + elif ve and not ee: + # Only vertex events. + # Merging of successive vertices in inset face will + # take care of the vertex events + newfaces = self.MakeNewFaces(self.endtime) else: - # A hole was split. New faces just replace the split one. - pa.poly = newfaces[0] - pa.holes = newfaces[0:findex] + [ newface0, newface1 ] + \ - newfaces[findex+1:] - else: - # A join - (_, findex, othfindex, newface0) = splitjoin - if findex == 0 or othfindex == 0: - # Outer poly was joined to one hole. - pa.poly = newface0 - pa.holes = [ f for f in newfaces if f is not None ] + # Edge events too + # First make the new faces (handles all vertex events) + newfaces = self.MakeNewFaces(self.endtime) + # Only do one edge event (handle other simultaneous edge + # events in subsequent recursive Build calls) + splitjoin = self.SplitJoinFaces(newfaces, ee[0]) + nexttarget = target - self.endtime + if len(newfaces) > 0: + pa = geom.PolyArea(points=self.polyarea.points) + pa.data = self.polyarea.data + newt = self.timesofar + self.endtime + pa2 = None # may make another + if not splitjoin: + pa.poly = newfaces[0] + pa.holes = newfaces[1:] + elif splitjoin[0] == 'split': + (_, findex, newface0, newface1) = splitjoin + if findex == 0: + # Outer poly of polyarea was split. + # Now there will be two polyareas. + # If there were holes, need to allocate according to + # which one contains the holes. + pa.poly = newface0 + pa2 = geom.PolyArea(points=self.polyarea.points) + pa2.data = self.polyarea.data + pa2.poly = newface1 + if len(newfaces) > 1: + # print("need to allocate holes") + for hf in newfaces[1:]: + if pa.ContainsPoly(hf, self.polyarea.points): + # print("add", hf, "to", pa.poly) + pa.holes.append(hf) + elif pa2.ContainsPoly(hf, self.polyarea.points): + # print("add", hf, "to", pa2.poly) + pa2.holes.append(hf) + else: + print("whoops, hole in neither poly!") + self.inneroffsets = [Offset(pa, newt, self.vspeed), \ + Offset(pa2, newt, self.vspeed)] + else: + # A hole was split. New faces just replace the split one. + pa.poly = newfaces[0] + pa.holes = newfaces[0:findex] + [newface0, newface1] + \ + newfaces[findex + 1:] + else: + # A join + (_, findex, othfindex, newface0) = splitjoin + if findex == 0 or othfindex == 0: + # Outer poly was joined to one hole. + pa.poly = newface0 + pa.holes = [f for f in newfaces if f is not None] + else: + # Two holes were joined + pa.poly = newfaces[0] + pa.holes = [f for f in newfaces if f is not None] + \ + [newface0] + self.inneroffsets = [Offset(pa, newt, self.vspeed)] + if pa2: + self.inneroffsets.append(Offset(pa2, newt, self.vspeed)) + if nexttarget > TOL: + for o in self.inneroffsets: + o.Build(nexttarget) + + def FaceAtSpokeEnds(self, f, t): + """Return a new face that is at the spoke ends of face f at time t. + + Also merges any adjacent approximately equal vertices into one vertex, + so returned list may be smaller than len(f). + Also sets the destindex fields of the spokes to the vertex they + will now end at. + + Args: + f: list of Spoke - one of self.faces + t: float - time in this offset + Returns: + list of int - indices into self.polyarea.points + (which has been extended with new ones) + """ + + newface = [] + points = self.polyarea.points + for i in range(0, len(f)): + s = f[i] + vcoords = s.EndPoint(t, points, self.vspeed) + v = points.AddPoint(vcoords) + if newface: + if v == newface[-1]: + s.destindex = len(newface) - 1 + elif i == len(f) - 1 and v == newface[0]: + s.destindex = 0 + else: + newface.append(v) + s.destindex = len(newface) - 1 + else: + newface.append(v) + s.destindex = 0 + s.dest = v + return newface + + def MakeNewFaces(self, t): + """For each face in this offset, make new face extending spokes + to time t. + + Args: + t: double - time + Returns: + list of list of int - list of new faces + """ + + ans = [] + for f in self.facespokes: + newf = self.FaceAtSpokeEnds(f, t) + if len(newf) > 2: + ans.append(newf) + return ans + + def SplitJoinFaces(self, newfaces, ev): + """Use event ev to split or join faces. + + Given ev, an edge event, use the ev spoke to split the + other spoke's inner edge. + If the ev spoke's face and other's face are the same, this splits the + face into two; if the faces are different, it joins them into one. + We have just made faces at the end of the spokes. + We have to remove the edge going from the other spoke to its + next spoke, and replace it with two edges, going to and from + the event spoke's destination. + General situation: + __ s ____ + c\ b\ | /a /e + \ \|/ / + f----------------g + / d \ + o/ \h + + where sd is the event spoke and of is the "other spoke", + hg is a spoke, and cf, fg. ge, ad, and db are edges in + the new inside face. + What we are to do is to split fg into two edges, with the + joining point attached where b,s,a join. + There are a bunch of special cases: + - one of split fg edges might have zero length because end points + are already coincident or nearly coincident. + - maybe c==b or e==a + + Args: + newfaces: list of list of int - the new faces + ev: OffsetEvent - an edge event + Side Effects: + faces in newfaces that are involved in split or join are + set to None + Returns: one of: + ('split', int, list of int, list of int) - int is the index in + newfaces of the face that was split, two lists are the + split faces + ('join', int, int, list of int) - two ints are the indices in + newfaces of the faces that were joined, and the list is + the joined face + """ + + # print("SplitJoinFaces", newfaces, ev) + spoke = ev.spoke + other = ev.other + findex = spoke.face + othfindex = other.face + newface = newfaces[findex] + othface = newfaces[othfindex] + nnf = len(newface) + nonf = len(othface) + d = spoke.destindex + f = other.destindex + c = (f - 1) % nonf + g = (f + 1) % nonf + e = (f + 2) % nonf + a = (d - 1) % nnf + b = (d + 1) % nnf + # print("newface=", newface) + # if findex != othfindex: print("othface=", othface) + # print("d=", d, "f=", f, "c=", c, "g=", g, "e=", e, "a=", a, "b=", b) + newface0 = [] + newface1 = [] + # The two new faces put spoke si's dest on edge between + # pi's dest and qi (edge after pi)'s dest in original face. + # These are indices in the original face; the current dest face + # may have fewer elements because of merging successive points + if findex == othfindex: + # Case where splitting one new face into two. + # The new new faces are: + # [d, g, e, ..., a] and [d, b, ..., c, f] + # (except we actually want the vertex numbers at those positions) + newface0 = [newface[d]] + i = g + while i != d: + newface0.append(newface[i]) + i = (i + 1) % nnf + newface1 = [newface[d]] + i = b + while i != f: + newface1.append(newface[i]) + i = (i + 1) % nnf + newface1.append(newface[f]) + # print("newface0=", newface0, "newface1=", newface1) + # now the destindex values for the spokes are messed up + # but I don't think we need them again + newfaces[findex] = None + return ('split', findex, newface0, newface1) else: - # Two holes were joined - pa.poly = newfaces[0] - pa.holes = [ f for f in newfaces if f is not None ] + [ newface0 ] - self.inneroffsets = [ Offset(pa, newt, self.vspeed) ] - if pa2: - self.inneroffsets.append(Offset(pa2, newt, self.vspeed)) - if nexttarget > TOL: - for o in self.inneroffsets: - o.Build(nexttarget) - - def FaceAtSpokeEnds(self, f, t): - """Return a new face that is at the spoke ends of face f at time t. + # Case where joining two faces into one. + # The new face is splicing d's face between + # f and g in other face (or the reverse of all of that). + newface0 = [othface[i] for i in range(0, f + 1)] + newface0.append(newface[d]) + i = b + while i != d: + newface0.append(newface[i]) + i = (i + 1) % nnf + newface0.append(newface[d]) + if g != 0: + newface0.extend([othface[i] for i in range(g, nonf)]) + # print("newface0=", newface0) + newfaces[findex] = None + newfaces[othfindex] = None + return ('join', findex, othfindex, newface0) + + def InnerPolyAreas(self): + """Return the interior of the offset (and contained offsets) as + PolyAreas. + + Returns: + geom.PolyAreas + """ + + ans = geom.PolyAreas() + ans.points = self.polyarea.points + _AddInnerAreas(self, ans) + return ans + + def MaxAmount(self): + """Returns the maximum offset amount possible. + Returns: + float - maximum amount + """ + + # Need to do Build on a copy of points + # so don't add points that won't be used when + # really do a Build with a smaller amount + test_points = geom.Points() + test_points.AddPoints(self.polyarea.points) + save_points = self.polyarea.points + self.polyarea.points = test_points + self.Build() + max_amount = self._MaxTime() + self.polyarea.points = save_points + return max_amount + + def _MaxTime(self): + if self.inneroffsets: + return max([o._MaxTime() for o in self.inneroffsets]) + else: + return self.timesofar + self.endtime - Also merges any adjacent approximately equal vertices into one vertex, - so returned list may be smaller than len(f). - Also sets the destindex fields of the spokes to the vertex they - will now end at. - Args: - f: list of Spoke - one of self.faces - t: float - time in this offset - Returns: - list of int - indices into self.polyarea.points (which has been extended with new ones) - """ +def _AddInnerAreas(off, polyareas): + """Add the innermost areas of offset off to polyareas. - newface = [] - points = self.polyarea.points - for i in range(0, len(f)): - s = f[i] - vcoords = s.EndPoint(t, points, self.vspeed) - v = points.AddPoint(vcoords) - if newface: - if v == newface[-1]: - s.destindex = len(newface) - 1 - elif i == len(f)-1 and v == newface[0]: - s.destindex = 0 - else: - newface.append(v) - s.destindex = len(newface) - 1 - else: - newface.append(v) - s.destindex = 0 - s.dest = v - return newface - - def MakeNewFaces(self, t): - """For each face in this offset, make new face extending spokes to time t. - - Args: - t: double - time - Returns: - list of list of int - list of new faces - """ + Assume that polyareas is already using the proper shared points. - ans = [] - for f in self.facespokes: - newf = self.FaceAtSpokeEnds(f, t) - if len(newf) > 2: - ans.append(newf) - return ans - - def SplitJoinFaces(self, newfaces, ev): - """Use event ev to split or join faces. - - Given ev, an edge event, use the ev spoke to split the - other spoke's inner edge. - If the ev spoke's face and other's face are the same, this splits the - face into two; if the faces are different, it joins them into one. - We have just made faces at the end of the spokes. - We have to remove the edge going from the other spoke to its - next spoke, and replace it with two edges, going to and from - the event spoke's destination. - General situation: - __ s ____ - c\ b\ | /a /e - \ \|/ / - f----------------g - / d \ - o/ \h - - where sd is the event spoke and of is the "other spoke", - hg is a spoke, and cf, fg. ge, ad, and db are edges in - the new inside face. - What we are to do is to split fg into two edges, with the - joining point attached where b,s,a join. - There are a bunch of special cases: - - one of split fg edges might have zero length because end points - are already coincident or nearly coincident. - - maybe c==b or e==a - - Args: - newfaces: list of list of int - the new faces - ev: OffsetEvent - an edge event + Arguments: + off: Offset + polyareas: geom.PolyAreas Side Effects: - faces in newfaces that are involved in split or join are - set to None - Returns: one of: - ('split', int, list of int, list of int) - int is the index in - newfaces of the face that was split, two lists are the - split faces - ('join', int, int, list of int) - two ints are the indices in - newfaces of the faces that were joined, and the list is - the joined face + Any non-zero-area faces in the very inside of off are + added to polyareas. """ - # print("SplitJoinFaces", newfaces, ev) - spoke = ev.spoke - other = ev.other - findex = spoke.face - othfindex = other.face - newface = newfaces[findex] - othface = newfaces[othfindex] - nnf = len(newface) - nonf = len(othface) - d = spoke.destindex - f = other.destindex - c = (f-1) % nonf - g = (f+1) % nonf - e = (f+2) % nonf - a = (d-1) % nnf - b = (d+1) % nnf - # print("newface=", newface) - # if findex != othfindex: print("othface=", othface) - # print("d=", d, "f=", f, "c=", c, "g=", g, "e=", e, "a=", a, "b=", b) - newface0 = [] - newface1 = [] - # The two new faces put spoke si's dest on edge between - # pi's dest and qi (edge after pi)'s dest in original face. - # These are indices in the original face; the current dest face - # may have fewer elements because of merging successive points - if findex == othfindex: - # Case where splitting one new face into two. - # The new new faces are: - # [d, g, e, ..., a] and [d, b, ..., c, f] - # (except we actually want the vertex numbers at those positions) - newface0 = [ newface[d] ] - i = g - while i != d: - newface0.append(newface[i]) - i = (i+1) % nnf - newface1 = [ newface[d] ] - i = b - while i != f: - newface1.append(newface[i]) - i = (i+1) % nnf - newface1.append(newface[f]) - # print("newface0=", newface0, "newface1=", newface1) - # now the destindex values for the spokes are messed up - # but I don't think we need them again - newfaces[findex] = None - return ('split', findex, newface0, newface1) + if off.inneroffsets: + for o in off.inneroffsets: + _AddInnerAreas(o, polyareas) else: - # Case where joining two faces into one. - # The new face is splicing d's face between - # f and g in other face (or the reverse of all of that). - newface0 = [ othface[i] for i in range(0, f+1) ] - newface0.append(newface[d]) - i = b - while i != d: - newface0.append(newface[i]) - i = (i+1) % nnf - newface0.append(newface[d]) - if g != 0: - newface0.extend([ othface[i] for i in range(g, nonf) ]) - # print("newface0=", newface0) - newfaces[findex] = None - newfaces[othfindex] = None - return ('join', findex, othfindex, newface0) - - - def InnerPolyAreas(self): - """Return the interior of the offset (and contained offsets) as PolyAreas. - - Returns: - geom.PolyAreas - """ - - ans = geom.PolyAreas() - ans.points = self.polyarea.points - _AddInnerAreas(self, ans) - return ans - - -def _AddInnerAreas(off, polyareas): - """Add the innermost areas of offset off to polyareas. - - Assume that polyareas is already using the proper shared points. - - Arguments: - off: Offset - polyareas: geom.PolyAreas - Side Effects: - Any non-zero-area faces in the very inside of off are - added to polyareas. - """ - - if off.inneroffsets: - for o in off.inneroffsets: - _AddInnerAreas(o, polyareas) - else: - newpa = geom.PolyArea(polyareas.points) - for i, f in enumerate(off.facespokes): - newface = off.FaceAtSpokeEnds(f, off.endtime) - area = abs(geom.SignedArea(newface, polyareas.points)) - if area < AREATOL: - if i == 0: - break - else: - continue - if i == 0: - newpa.poly = newface - newpa.color = off.polyarea.color - else: - newpa.holes.append(newface) - if newpa.poly: - polyareas.polyareas.append(newpa) - - + newpa = geom.PolyArea(polyareas.points) + for i, f in enumerate(off.facespokes): + newface = off.FaceAtSpokeEnds(f, off.endtime) + area = abs(geom.SignedArea(newface, polyareas.points)) + if area < AREATOL: + if i == 0: + break + else: + continue + if i == 0: + newpa.poly = newface + newpa.data = off.polyarea.data + else: + newpa.holes.append(newface) + if newpa.poly: + polyareas.polyareas.append(newpa) |