Blah blah blah.
Blah blah blah.















Pt = namedtuple("Pt", "x, y")
zigzags = [
[Pt(10, 20), Pt(15, 21), Pt(20, 25)],
[Pt(20, 25), Pt(30, 15), Pt(27, 25), Pt(23, 21)],
]
zz_by_ends = {
Pt(10, 20): [
[Pt(10, 20), Pt(15, 21), Pt(20, 25)],
],
Pt(20, 25): [
[Pt(10, 20), Pt(15, 21), Pt(20, 25)],
[Pt(20, 25), Pt(30, 15), Pt(27, 25), Pt(23, 21)],
],
Pt(23, 21): [
[Pt(20, 25), Pt(30, 15), Pt(27, 25), Pt(23, 21)],
],
}
>>> d = {}
>>> d['hi'] = 'hello'
>>> d['bye'] = 'goodbye'
>>> d['hi']
'hello'
>>> d = {}
>>> d[17] = 23
>>> d[17]
23
>>> d[(1, 2)] = 'a point'
>>> d[(1, 2)]
'a point'
>>> d[(1.0, 2.0)]
'a point'
>>> d[Pt(10, 20)] = 'another point'
>>> d[Pt(10, 20)]
'another point'
zigzags = [
[Pt(10, 20), Pt(15, 21), Pt(20, 25)],
[Pt(20, 25), Pt(30, 15), Pt(27, 25), Pt(23, 21)],
[Pt(10, 20.00001), Pt(15, 40)], # !!! :(
...
]
>>> d[Pt(10, 20)] = "hey there"
>>> d[Pt(10, 20.00001)]
KeyError: Pt(10, 20.00001)
>>> Pt(10, 20) == Pt(10, 20.00001)
False
>>> 20 == 20.00001
False
>>> 0.1 + 0.2 == 0.3
False
class Pt(namedtuple("Pt", "x, y")):
def __eq__(self, other):
x1, y1 = self
x2, y2 = other
return (
math.isclose(x1, x2) and
math.isclose(y1, y2)
)
class Pt(namedtuple("Pt", "x, y")):
def __eq__(self, other):
x1, y1 = self
x2, y2 = other
return (
math.isclose(x1, x2) and
math.isclose(y1, y2)
)
def __hash__(self):
# ¯\_(ツ)_/¯
zz_by_ends = [
(Pt(10, 20), [..zigzags..] ),
(Pt(20, 25), [..zigzags..] ),
(Pt(23, 21), [..zigzags..] ),
]
def find_zigzags(p):
for pt, zigzags in zz_by_ends:
if p == pt: # fuzzy!
return zigzags
class PointDict:
def __init__(self):
# A list of (pt, value) tuples.
self.items = []
# pd[pt] ≡ pd.__getitem__(pt)
def __getitem__(self, pt):
for key, value in self.items:
if key == pt: # fuzzy!
return value
# Didn't find it: make a new list value and add
# it to the end.
value = []
self.items.append((pt, value))
return value
pd = PointDict()
pd[pt1].append(zz1)
print(pd[pt1])
def __getitem__(self, pt):
for key, value in self.items: # N/2 + N/2
if key == pt: # N/2
return value # 1
pd = PointDict() # Actually, slow linear search!
for zz in all_zigzags:
others = pd[zz[0]]
| Lists | |
|---|---|
| mylist.append(val) | O(1) |
| mylist[i] | O(1) |
| val in mylist | O(N) |
| for val in mylist: | O(N) |
| Dicts | |
|---|---|
| mydict[key] = val | O(1) |
| mydict[key] | O(1) |
| key in mydict | O(1) |
| for key in mydict: | O(N) |
| Sets | |
|---|---|
| myset.add(val) | O(1) |
| val in myset | O(1) |
| for val in myset: | O(N) |
class Pt(namedtuple("Pt", "x, y")):
def rounded(self):
return (round(self.x), round(self.y))
def __eq__(self, other):
return self.rounded() == other.rounded()
def __hash__(self):
return hash(self.rounded())
# BUT!
>>> Pt(11.4999999, 0) == Pt(11.5000001, 0)
False
# Because (11.0, 0) != (12.0, 0)
def rounded(pt, jitter):
return round(pt.x + jitter), round(pt.y + jitter)
def pt_equal(p1, p2):
return (
rounded(p1, 0) == rounded(p2, 0) or
rounded(p1, 0.5) == rounded(p2, 0.5)
)
>>> pt_equal(Pt(10.9999999, 0), Pt(11.0000001, 0))
True
>>> pt_equal(Pt(11.4999999, 0), Pt(11.5000001, 0))
True
class PointDict:
"""Like defaultdict(list), with fuzzy Pt's as keys."""
def __init__(self):
self.items = {} # pt -> value
self.rounds = {} # pt -> pt
def __getitem__(self, pt): # O(1)
val = self.items.get(pt)
if val is not None:
return val
for jitter in [0, 0.5]:
pt_rnd = rounded(pt, jitter)
pt0 = self.rounds.get(pt_rnd)
if pt0 is not None:
return self.items[pt0]
# Not found: make an empty list value
self.items[pt] = val = []
for jitter in [0, 0.5]:
pt_rnd = rounded(pt, jitter)
self.rounds[pt_rnd] = pt
return val
~2100 points
slow: 20 sec
fast: 0.4 sec!
def __init__(self):
self.items = []
def __getitem__(self, pt):
for key, value in self.items:
if key == pt:
return value
value = []
self.items.append((pt, value))
return value
O(1)
O(n)
def __init__(self):
self.items = []
def __getitem__(self, pt):
for key, value in self.items:
if key == pt:
return value
value = []
self.items.append((pt, value))
return value
def __init__(self):
self.items = {} # pt -> value
self.rounds = {} # pt -> pt
def __getitem__(self, pt):
val = self.items.get(pt)
if val is not None:
return val
for jitter in [0, 0.5]:
pt_rnd = rounded(pt, jitter)
pt0 = self.rounds.get(pt_rnd)
if pt0 is not None:
return self.items[pt0]
self.items[pt] = val = []
for jitter in [0, 0.5]:
pt_rnd = rounded(pt, jitter)
self.rounds[pt_rnd] = pt
return val
def __init__(self):
self.items = {} # pt -> value
self.rounds = {} # pt -> pt
def __getitem__(self, pt):
val = self.items.get(pt)
if val is not None:
return val
for jitter in [0, 0.5]:
pt_rnd = rounded(pt, jitter)
pt0 = self.rounds.get(pt_rnd)
if pt0 is not None:
return self.items[pt0]
self.items[pt] = val = []
for jitter in [0, 0.5]:
pt_rnd = rounded(pt, jitter)
self.rounds[pt_rnd] = pt
return val
class Defuzzer:
def defuzz(self, pt):
"""Return pt, or a previous equal point."""
>>> defuzz = Defuzzer().defuzz
>>> defuzz(Pt(1, 2))
Pt(1, 2)
>>> defuzz(Pt(3, 4))
Pt(3, 4)
>>> defuzz(Pt(1.01, 2))
Pt(1, 2)
def defuzz_path(path):
return [defuzz(pt) for pt in path]
def defuzz_paths(paths):
return [defuzz_path(path) for path in paths]
def combine_paths(paths):
paths = defuzz_paths(paths)
paths_by_ends = defaultdict(list)
for path in paths:
paths_by_ends[path[0]].append(path)
paths_by_ends[path[-1]].append(path)
for path in paths:
others = paths_by_ends[path[0]]
etc()
path = [Pt(0, 0), Pt(1, 1), Pt(10, 0)]
def path_length(path): ...
def canonicalize_path(path): ...
def offset_path(path, offset): ...
class Path:
def __init__(self, points):
self.points = points
def length(self): ...
def canonicalize(self): ...
def offset(self, offset): ...
if path[0] == path[-1]:
class Path:
...
@property
def closed(self):
return self.points[0] == self.points[-1]
if path.closed:
def paths_length(paths):
return sum(p.length() for p in paths)
def defuzz_paths(paths):
return [p.defuzz() for p in paths]
>>> from collections import namedtuple
>>> Pt = namedtuple("Pt", "x, y") # Like "class Pt:"
>>> p = Pt(10, 20)
>>> p
Pt(x=10, y=20)
>>> p.x
10
>>> x, y = p # Unpacking the tuple
>>> x
10
>>> p.x = 17
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: can't set attribute
>>> import attr
>>> @attr.s
... class Pt(object):
... x = attr.ib()
... y = attr.ib()
...
>>> p = Pt(10, 20)
>>> p
Pt(x=10, y=20)
def segment_intersections(segments):
isects = defaultdict(list)
for seg1 in segments:
for seg2 in segments:
if seg1 is seg2:
continue
isect_pt = seg1.intersect(seg2)
if isect_pt is not None:
isects[isect_pt].extend([seg1, seg2])
return isects
File ".../poly_point_isect.py", line 576, in isect_segments_impl
sweep_line.handle(p, events_current)
File ".../poly_point_isect.py", line 384, in handle
self.handle_event(e)
File ".../poly_point_isect.py", line 408, in handle_event
self.remove(event)
File ".../poly_point_isect.py", line 346, in remove
assert(event.in_sweep == False)
AssertionError
def intersections():
for angle in [x/6 for x in range(60)]:
rotate_everything(angle)
try:
isects = isect_segments()
except AssertionError:
print(f"Failed at rotation {angle}")
continue
rotate_everything(-angle)
return isects
Failed at rotation 0.0
Failed at rotation 0.1666666666
Failed at rotation 0.3333333333
Failed at rotation 0.5
Failed at rotation 0.6666666666
Failed at rotation 0.8333333334
Failed at rotation 1.0
Failed at rotation 1.1666666667
1728 intersections
def all_pairs(seq):
"""Produce all pairs from seq, but not (a, a)"""
return itertools.combinations(seq, 2)
"""Strappiness for Zellij."""
import collections
import itertools
import random
from zellij.debug import should_debug
from zellij.drawing import Drawing, DrawingSequence
from zellij.euclid import Point, Segment
from zellij.intersection import segment_intersections
from zellij.path_tiler import join_paths, offset_path, path_segments, trim_path
class Xing:
def __init__(self, under=None, over=None):
self.under = under
self.over = over
self.over_piece = None
def __repr__(self):
return f"<Xing under={show_path(self.under)} over={show_path(self.over)}>"
class Strap:
def __init__(self, path, width, random_factor=0):
self.path = path
if random_factor:
width *= (1 + random.random() * random_factor)
self.sides = [offset_path(path, d) for d in [width/2, -width/2]]
def __repr__(self):
return f"<Strap path={self.path}>"
def path_pieces(path, segs_to_points):
"""Produce a new series of paths, split at intersection points.
Yields a series of pieces (paths). The pieces trace the same line as the
original path. The endpoints of the pieces are all intersection points
in `segs_to_points`, or the endpoints of the original path, if it isn't
circular. The pieces are in order along `path`, so consecutive pieces
end and begin at the same point. If `path` is closed, then the first piece
returned will begin at the first cut, not at the path's first point.
"""
# If path is circular, then the first piece we collect has to be added to
# the last piece, so save it for later.
collecting_head = (path[0] == path[-1])
head = None
piece = []
for pt in path:
if not piece:
piece.append(pt)
else:
seg = Segment(piece[-1], pt)
cuts = segs_to_points.get(seg)
if cuts is not None:
cuts = seg.sort_along(cuts)
for cut in cuts:
ptcut = Point(*cut)
piece.append(ptcut)
if collecting_head:
head = piece
collecting_head = False
else:
yield piece
piece = [ptcut]
piece.append(pt)
if head:
piece = join_paths(piece, head)
yield piece
def pieces_under_over(path, segs_to_points, xings):
"""Produce all the pieces of the path, with a bool indicating if each leads to under
pieces = list(path_pieces(path, segs_to_points))
for i, piece in enumerate(pieces):
xing = xings.get(piece[-1])
if xing is None:
continue
if xing.under == path:
over = False
elif xing.over == path:
over = True
elif xing.under is not None:
over = True
else:
assert xing.over is not None
over = False
ou = [over, not over]
if i % 2:
ou = ou[::-1]
break
else:
ou = [True, False]
yield from zip(pieces, itertools.cycle(ou))
def strapify(paths, **strap_kwargs):
"""Turn paths intro straps."""
paths = [tuple(path) for path in paths]
segments = []
segs_to_paths = {}
for path in paths:
for segment in path_segments(path):
segments.append(segment)
segs_to_paths[segment] = path
points_to_segments = segment_intersections(segments)
isect_points = list(points_to_segments.keys())
segs_to_points = collections.defaultdict(list)
for pt, segs in points_to_segments.items():
for seg in segs:
segs_to_points[seg].append(pt)
points_to_paths = collections.defaultdict(list)
for isect, segs in points_to_segments.items():
for seg in segs:
points_to_paths[isect].append(segs_to_paths[seg])
print(f"{len(isect_points)} intersections")
if 0:
debug_output(dwgw=DWGW, paths=paths, segments=segments, isects=isect_points)
debug = should_debug("strapify")
if debug:
dbgdwgs = iter(DrawingSequence(name="debugs_", paths=paths))
paths_to_do = set(paths)
paths_done = set()
xings = {} # pt -> xing
straps = [] # new smaller paths, ending at unders.
path = None
while paths_to_do:
next_paths = set()
next_paths.add(paths_to_do.pop())
while next_paths:
previous_path = path
path = next_paths.pop()
if debug:
dwg = next(dbgdwgs)
dwg.draw_segments(segments, rgb=(0, 0, 0), width=1)
dwg.draw_paths(paths_done, rgb=(0, 0, 0), width=3)
dwg.draw_paths(next_paths, rgb=(.7, .7, 0), width=9)
if previous_path:
dwg.draw_path(previous_path, rgb=(1, 0, 0), width=10, dash=[30, 30])
dwg.draw_path(path, rgb=(1, 0, 0), width=15)
dwg.fill_points([path[0]], rgb=(1, 0, 0), radius=15*3/2)
dwg.fill_points([path[1]], rgb=(1, 0, 0), radius=15*2/2)
partial_over = [pt for pt, xing in xings.items() if xing.under is None]
partial_under = [pt for pt, xing in xings.items() if xing.over is None]
dwg.circle_points(partial_over, rgb=(.8, 0, 0), radius=21, width=9)
dwg.circle_points(partial_under, rgb=(0, 0, .8), radius=21, width=9)
done = [pt for pt, xing in xings.items() if xing.under is not None and x
dwg.circle_points(done, rgb=(0, .8, 0), radius=15, width=3)
prev_piece = None
last_cut = None
first_piece = True
for piece, over in pieces_under_over(path, segs_to_points, xings):
if first_piece and debug:
dwg.cross_points([piece[0], piece[-1]], radius=20, rgb=(1, 0, 0), wi
dwg.finish()
if 0 and dwg.num > 10:
print()
import sys; sys.exit()
first_piece = False
cut = None
if over:
assert prev_piece is None
prev_piece = piece
if last_cut:
cut = last_cut
xing = xings.get(cut)
if xing is None:
xing = Xing(under=path)
xings[cut] = xing
else:
assert xing.under is None or xing.under == path
xing.under = path
last_cut = None
else:
if prev_piece:
cut = prev_piece[-1]
assert cut == piece[0]
strap = Strap(join_paths(prev_piece, piece), **strap_kwargs)
straps.append(strap)
xing = xings.get(cut)
if xing is None:
xing = Xing(over=path)
xings[cut] = xing
else:
assert xing.over is None or xing.over == path
xing.over = path
xing.over_piece = strap
else:
straps.append(Strap(piece, **strap_kwargs))
last_cut = piece[-1]
prev_piece = None
if cut:
for next_path in points_to_paths[cut]:
if next_path in paths_to_do:
paths_to_do.remove(next_path)
next_paths.add(next_path)
if prev_piece:
strap = Strap(prev_piece, **strap_kwargs)
straps.append(strap)
closed = (path[0] == path[-1])
if closed:
cut = prev_piece[-1]
xing = xings.get(cut)
if xing is None:
xing = Xing(over=path)
xings[cut] = xing
else:
xing.over = path
xing.over_piece = strap
paths_done.add(path)
if debug:
dwg.cross_points([piece[0], piece[-1]], radius=30, rotate=30, rgb=(0, 0,
dwg.finish()
if 0 and dwg.num > 10:
print()
import sys; sys.exit()
if 0:
bad = [pt for pt, xing in xings.items() if xing.over_piece is None]
debug_output(dwgw=DWGW, paths=paths, segments=segments, isects=bad)
if debug:
for strap in straps:
dwg = next(dbgdwgs)
dwg.draw_segments(segments, rgb=(0, 0, 0), width=1)
dwg.draw_path(strap.path, rgb=(1, 0, 0), width=3)
for s in strap.sides:
dwg.draw_path(s, rgb=(0, 0, 1), width=1)
dwg.finish()
for strap in straps:
for end in [0, -1]:
xing = xings.get(strap.path[end])
if xing is not None:
trimmers = xing.over_piece.sides
strap.sides = [trim_path(s, end, trimmers) for s in strap.sides]
return straps
def debug_output(dwgw=None, paths=None, segments=None, isects=None):
dwg = Drawing(paths=paths, name="debug.png")
dwg.draw_segments(segments, rgb=(0, 0, 0), width=1)
dup_segments = []
segments.sort()
for s1, s2 in zip(segments, segments[1:]):
if s1 == s2:
dup_segments.append(s1)
dwg.draw_segments(dup_segments, rgb=(1, 0, 0), width=7)
if dwgw is not None:
with dwg.style(rgb=(0, 0, 1), width=2, dash=[5, 5]):
dwg.rectangle(0, 0, dwgw, dwgw)
dwg.stroke()
if isects is not None:
dwg.circle_points(isects, radius=9, rgb=(0, .5, 0), width=3)
dwg.finish()