499 lines
21 KiB
Python
499 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import re
|
|
import colorsys
|
|
import textwrap
|
|
import math
|
|
from pathlib import Path
|
|
import webbrowser
|
|
|
|
import click
|
|
|
|
tau = 2*math.pi
|
|
|
|
|
|
class Tag:
|
|
""" Helper class to ease creation of SVG. All API functions that create SVG allow you to substitute this with your
|
|
own implementation by passing a ``tag`` parameter. """
|
|
|
|
def __init__(self, name, children=None, root=False, **attrs):
|
|
if (fill := attrs.get('fill')) and isinstance(fill, tuple):
|
|
attrs['fill'], attrs['fill-opacity'] = fill
|
|
if (stroke := attrs.get('stroke')) and isinstance(stroke, tuple):
|
|
attrs['stroke'], attrs['stroke-opacity'] = stroke
|
|
self.name, self.attrs = name, attrs
|
|
self.children = children or []
|
|
self.root = root
|
|
|
|
def __str__(self):
|
|
prefix = '<?xml version="1.0" encoding="utf-8"?>\n' if self.root else ''
|
|
opening = ' '.join([self.name] + [f'{key.lstrip("_").replace("__", ":").replace("_", "-")}="{value}"' for key, value in self.attrs.items()])
|
|
if self.children:
|
|
children = '\n'.join(textwrap.indent(str(c), ' ') for c in self.children)
|
|
return f'{prefix}<{opening}>\n{children}\n</{self.name}>'
|
|
else:
|
|
return f'{prefix}<{opening}/>'
|
|
|
|
|
|
@classmethod
|
|
def setup_svg(kls, tags, bounds, margin=0, unit='mm', pagecolor='white', inkscape=False):
|
|
(min_x, min_y), (max_x, max_y) = bounds
|
|
|
|
if margin:
|
|
min_x -= margin
|
|
min_y -= margin
|
|
max_x += margin
|
|
max_y += margin
|
|
|
|
w, h = max_x - min_x, max_y - min_y
|
|
w = 1.0 if math.isclose(w, 0.0) else w
|
|
h = 1.0 if math.isclose(h, 0.0) else h
|
|
|
|
if inkscape:
|
|
tags.insert(0, kls('sodipodi:namedview', [], id='namedview1', pagecolor=pagecolor,
|
|
inkscape__document_units=unit))
|
|
namespaces = dict(
|
|
xmlns="http://www.w3.org/2000/svg",
|
|
xmlns__xlink="http://www.w3.org/1999/xlink",
|
|
xmlns__sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
|
|
xmlns__inkscape='http://www.inkscape.org/namespaces/inkscape')
|
|
|
|
else:
|
|
namespaces = dict(
|
|
xmlns="http://www.w3.org/2000/svg",
|
|
xmlns__xlink="http://www.w3.org/1999/xlink")
|
|
|
|
return kls('svg', tags,
|
|
width=f'{w}{unit}', height=f'{h}{unit}',
|
|
viewBox=f'{min_x} {min_y} {w} {h}',
|
|
style=f'background-color:{pagecolor}',
|
|
**namespaces,
|
|
root=True)
|
|
|
|
|
|
def dimension(p1, p2, h=5, line_offset=1, arrow_offset=.5, inner_arrow_min=5, arrow_head_size=1, arrow_length=3,
|
|
connecting_line_offset=1, color='black', line_width=0.2, text_offset=3, text_offset_max=15,
|
|
text_color=None, text_format='{:.2f} mm', font_size=2, offset_1=0, offset_2 = 0):
|
|
if text_color is None:
|
|
text_color = color
|
|
|
|
x1, y1 = p1
|
|
x2, y2 = p2
|
|
dx, dy = x2-x1, y2-y1
|
|
l = math.hypot(dx, dy)
|
|
nx, ny = -dy/l, dx/l
|
|
ax, ay = dx/l, dy/l
|
|
|
|
if l > inner_arrow_min:
|
|
xa1, ya1 = x1 + ax*arrow_offset, y1 + ay*arrow_offset
|
|
xa2, ya2 = x2 - ax*arrow_offset, y2 - ay*arrow_offset
|
|
cl_off = 2*arrow_offset + arrow_length
|
|
else:
|
|
xa2, ya2 = x1 - ax*arrow_offset, y1 - ay*arrow_offset
|
|
xa1, ya1 = x2 + ax*arrow_offset, y2 + ay*arrow_offset
|
|
cl_off = 2*arrow_offset
|
|
|
|
angle = math.degrees(math.atan2(dy, dx))
|
|
|
|
c_off = h - connecting_line_offset
|
|
cl_off = min(l/2, cl_off)
|
|
yield Tag('path', fill='none', stroke=color, stroke_width=line_width,
|
|
d=f'''
|
|
M {x1-nx*line_width/2},{y1-ny*line_width/2} l {nx*line_width},{ny*line_width}
|
|
M {x1+nx*(line_offset + offset_1)},{y1+ny*(line_offset+offset_1)} l {nx*(h-offset_1)},{ny*(h-offset_1)}
|
|
M {x2-nx*line_width/2},{y2-ny*line_width/2} l {nx*line_width},{ny*line_width}
|
|
M {x2+nx*(line_offset+offset_2)},{y2+ny*(line_offset+offset_2)} l {nx*(h-offset_2)},{ny*(h-offset_2)}
|
|
|
|
M {x1+nx*c_off + ax*cl_off},{y1+ny*c_off+ay*cl_off}
|
|
l {ax*(l-2*cl_off)},{ay*(l-2*cl_off)}
|
|
''')
|
|
|
|
xa1, ya1 = xa1 + nx*c_off, ya1 + ny*c_off
|
|
xa2, ya2 = xa2 + nx*c_off, ya2 + ny*c_off
|
|
|
|
aw, al = arrow_head_size, arrow_head_size*2
|
|
ab = aw*0.2
|
|
for xa, ya, al, arrow_length, abl in [(xa1, ya1, al, arrow_length, ab), (xa2, ya2, -al, -arrow_length, -ab)]:
|
|
yield Tag('path', fill=color, stroke=color, stroke_width=line_width,
|
|
d=f'''
|
|
M {xa},{ya}
|
|
l {nx*aw/2 + ax*al},{ny*aw/2 + ay*al}
|
|
l {-nx*aw/2 - ax*abl},{-ny*aw/2 - ay*abl}
|
|
l {ax*(arrow_length-al+abl)},{ay*(arrow_length-al+abl)}
|
|
l {-ax*(arrow_length-al+abl)},{-ay*(arrow_length-al+abl)}
|
|
l {-nx*aw/2 + ax*abl},{-ny*aw/2 + ay*abl}
|
|
Z
|
|
''')
|
|
|
|
if not (-90 <= angle < 90):
|
|
angle = (angle - 180) % 360
|
|
baseline = 'auto'
|
|
else:
|
|
baseline = 'hanging'
|
|
|
|
if l > text_offset_max:
|
|
text_offset = 0
|
|
baseline = 'middle'
|
|
|
|
tx = x1 + nx*c_off + ax*l/2 + nx*text_offset
|
|
ty = y1 + ny*c_off + ay*l/2 + ny*text_offset
|
|
for text_color, stroke in [('white', 'white'), (text_color, 'none')]:
|
|
yield Tag('text', children=[text_format.format(l)],
|
|
x=tx, y=ty,
|
|
font_family='sans-serif', font_size=f'{font_size:.3f}px',
|
|
text_anchor='middle', dominant_baseline=baseline,
|
|
transform=f'rotate({angle},{tx},{ty})',
|
|
stroke=stroke, stroke_width=1,
|
|
stroke_linejoin='round',
|
|
fill=text_color)
|
|
|
|
def angular_dimension(p, a1, a2, large_arc=None, r=3, line_offset=1, arrow_offset=.5, inner_arrow_min=5, arrow_head_size=1, arrow_length=3,
|
|
connecting_line_offset=1, color='black', line_width=0.2, text_offset=2, text_offset_max=15,
|
|
text_color=None, text_format='{:.1f} °', font_size=2, offset_1=0, offset_2 = 0):
|
|
|
|
text_color= text_color if text_color else color
|
|
da = a2 - a1
|
|
da = (da + math.pi) % (2*math.pi) - math.pi
|
|
|
|
arrow_offset = math.asin(arrow_offset/(2*r))
|
|
if da < 0 == large_arc:
|
|
arrow_offset = -arrow_offset
|
|
|
|
if abs(da) > 2*arrow_offset:
|
|
yield Tag('path', fill='none', stroke_width=line_width, stroke=color, transform=f'translate({p[0]} {p[1]})',
|
|
d=f'''M {math.cos(a1 + arrow_offset)*r} {math.sin(a1 + arrow_offset)*r}
|
|
A {r} {r} 0 {int(bool(large_arc))} {int(bool(large_arc))} {math.cos(a2 - arrow_offset)*r} {math.sin(a2 - arrow_offset)*r}
|
|
''')
|
|
|
|
tx = p[0] + math.cos(a1 + da/2) * (r + text_offset)
|
|
ty = p[1] + math.sin(a1 + da/2) * (r + text_offset)
|
|
|
|
ta = (math.degrees(a1+da/2) + 90 + 180) % 360 - 180
|
|
if not (-90 <= ta < 90):
|
|
ta = (ta - 180) % 360
|
|
|
|
for text_color, stroke in [('white', 'white'), (text_color, 'none')]:
|
|
yield Tag('text', children=[text_format.format(abs(math.degrees(da)))],
|
|
x=tx, y=ty,
|
|
font_family='sans-serif', font_size=f'{font_size:.3f}px',
|
|
text_anchor='middle', dominant_baseline='middle',
|
|
transform=f'rotate({ta},{tx},{ty})',
|
|
stroke=stroke, stroke_width=1,
|
|
stroke_linejoin='round',
|
|
fill=text_color)
|
|
|
|
crosshairs = lambda pt, s=2, color='black', xf='': Tag('path', fill='none', stroke_width=0.2, stroke=color, transform=xf,
|
|
d=f'M {pt[0]-s},{pt[1]} h {2*s} m {-s},{s} v {-2*s}')
|
|
circle = lambda pt, r, c, w=0.2, op=1.0: Tag('circle', stroke=c, fill='none', stroke_width=w, cx=pt[0], cy=pt[1], r=r, stroke_opacity=op)
|
|
|
|
hls_svg = lambda h, s, l: '#'+''.join(f'{round(c*255):02x}' for c in colorsys.hls_to_rgb(h/360, l/100, s/100))
|
|
|
|
red = lambda s, l: hls_svg(0, l, s)
|
|
blue = lambda s, l: hls_svg(240, l, s)
|
|
|
|
@click.command()
|
|
@click.option('-r', '--radius', default='50/50')
|
|
@click.option('-n', '--num-meshes', default='5/3')
|
|
@click.option('-w', '--mesh-width', default='10/10')
|
|
@click.option('-t', '--mesh-thickness', default='1.6/1.6')
|
|
@click.option('-o', '--offset', type=float, default=20)
|
|
@click.option('-p', '--phase', type=float, default=0)
|
|
@click.option('--counterrotation/--corotation')
|
|
@click.argument('out_svg_1', type=click.Path(dir_okay=False, path_type=Path))
|
|
@click.argument('out_svg_2', type=click.Path(dir_okay=False, path_type=Path))
|
|
@click.argument('out_svg_3', type=click.Path(dir_okay=False, path_type=Path))
|
|
def cli(out_svg_1, out_svg_2, out_svg_3, radius, num_meshes, mesh_width, mesh_thickness, offset, phase, counterrotation):
|
|
tags = []
|
|
dim_tags = []
|
|
plots = []
|
|
|
|
def parse_split(s, parse=float, extra_args=((), ())):
|
|
if not isinstance(s, str):
|
|
return s, s
|
|
|
|
l, _, r = s.partition('/')
|
|
l = parse(l, *extra_args[0])
|
|
r = parse(r, *extra_args[1]) if r else l
|
|
return l, r
|
|
|
|
r1, r2 = parse_split(radius)
|
|
assert r1 > 0 and r2 > 0
|
|
assert offset > 0
|
|
assert offset-r2 < r1 < offset+r2
|
|
|
|
th1, th2 = parse_split(mesh_thickness)
|
|
assert th1 > 0 and th2 > 0
|
|
|
|
n1, n2 = parse_split(num_meshes, int)
|
|
assert n1 > 0 and n2 > 0
|
|
|
|
def parse_mesh_width(s, r, th):
|
|
num, unit = re.fullmatch(r'([0-9.]+)\s*(|deg|degree|mm|cm)', s).groups()
|
|
num = float(num)
|
|
|
|
if unit in ('', 'deg', 'degree'):
|
|
return num
|
|
|
|
r -= th/2
|
|
out = math.degrees(math.atan(num/2 / r))
|
|
print(f'Calculated mesh angle for mesh width {num:.1f}{unit} and thickness {th:.1f}mm at radius {r:.1f}mm: {out:.1f}°')
|
|
return out
|
|
|
|
mesh_w1, mesh_w2 = parse_split(mesh_width, parse=parse_mesh_width, extra_args=((r1, th1), (r2, th2)))
|
|
assert mesh_w1 > 0 and mesh_w2 > 0
|
|
mesh_w1, mesh_w2 = math.radians(mesh_w1), math.radians(mesh_w2)
|
|
|
|
assert n1 * mesh_w1 < 2*math.pi
|
|
assert n2 * mesh_w2 < 2*math.pi
|
|
|
|
# https://mathworld.wolfram.com/Circle-CircleIntersection.html
|
|
chord = 1/offset * math.sqrt(4 * offset**2 * r1**2 - (offset**2 - r2**2 + r1**2)**2)
|
|
x = (offset**2 - r2**2 + r1**2) / (2*offset)
|
|
a1 = math.asin(chord/(2*r1))
|
|
a2 = math.asin(chord/(2*r2))
|
|
if offset > x:
|
|
a2 = math.pi - a2
|
|
|
|
c1, c2 = red(40, 70), blue(40, 70)
|
|
center_1, center_2 = (0, 0), (offset, 0)
|
|
|
|
tags.append(circle(center_1, r1, c1))
|
|
tags.append(circle(center_2, r2, c2))
|
|
dim_tags += dimension((x, chord/2), (x, -chord/2), h=offset/2+5)
|
|
tags += angular_dimension(center_1, a1, -a1, large_arc=False)
|
|
tags += angular_dimension(center_2, -a2 if offset > x else a2, a2 if offset > x else -a2, large_arc=False)
|
|
|
|
tags.append(Tag('path', fill='none', stroke_width=0.2, stroke=c1, d=f'M {x} {chord/2} L 0 0 L {x} {-chord/2}'))
|
|
tags.append(Tag('path', fill='none', stroke_width=0.2, stroke=c2, d=f'M {x} {chord/2} L {offset} 0 L {x} {-chord/2}'))
|
|
tags.append(Tag('path', fill='none', stroke_width=0.2, stroke='black', d=f'M {x} {chord/2} l 0 {-chord}'))
|
|
|
|
out_svg_1.write_text(str(Tag.setup_svg(tags + dim_tags, bounds=((-r1, -max(r1, r2)), (offset+r2, max(r1, r2))), margin=3)))
|
|
|
|
tags = []
|
|
dim_tags = []
|
|
|
|
# https://stackoverflow.com/questions/13069446/simple-fill-pattern-in-svg-diagonal-hatching
|
|
defs = []
|
|
defs.append(Tag('pattern', [Tag('path', stroke=c1, stroke_width=1,
|
|
d='M -1,1 l 2,-2 M 0,4 l 4,-4 M 3,5 l2,-2')],
|
|
id='hatch_1', patternUnits='userSpaceOnUse', width='4', height='4', patternTransform='rotate(-45) scale(0.15)'))
|
|
defs.append(Tag('pattern', [Tag('path', stroke=c2, stroke_width=1,
|
|
d='M -1,1 l 2,-2 M 0,4 l 4,-4 M 3,5 l2,-2')],
|
|
id='hatch_2', patternUnits='userSpaceOnUse', width='4', height='4', patternTransform='scale(0.15)'))
|
|
defs.append(Tag('pattern', [Tag('path', stroke='black', stroke_width=1,
|
|
d='M -1,1 l 2,-2 M 0,4 l 4,-4 M 3,5 l2,-2')],
|
|
id='hatch_b', patternUnits='userSpaceOnUse', width='4', height='4', patternTransform='scale(0.15)'))
|
|
tags.append(Tag('defs', defs))
|
|
|
|
|
|
def plot_angles(angles, color='black', fill='lightgray', title=None, h=10, w=100, minor_ivl=5, major_ivl=45,
|
|
minor_len=.8, major_len=1.2, tick_label_sp=1, tick_format='{:.0f}°', font_size=2, title_offset=-2,
|
|
axes=True, axes_stroke=0.15, tick_stroke=0.15, box_stroke=0.15):
|
|
if axes:
|
|
yield Tag('path', fill='none', stroke_width=axes_stroke, stroke='black', d=f'''
|
|
M 0 0 v {h} h {w} v {-h} Z
|
|
''')
|
|
|
|
assert math.isclose(major_ivl % minor_ivl, 0, abs_tol=1e-6)
|
|
num_minor = round(major_ivl/minor_ivl)
|
|
|
|
ticks = [(i*minor_ivl, i%num_minor == 0)
|
|
for i in range(int(360.001/minor_ivl) + 1)]
|
|
yield Tag('path', fill='none', stroke_width=tick_stroke, stroke='black', d=' '.join(
|
|
f'M {w*a/360} {h} v {major_len if major else minor_len}' for a, major in ticks
|
|
))
|
|
|
|
for a, major in ticks:
|
|
if not major:
|
|
continue
|
|
|
|
tx, ty = w*a/360, h + major_len + tick_label_sp
|
|
for text_color, stroke in [('white', 'white'), ('black', 'none')]:
|
|
yield Tag('text', children=[tick_format.format(a)],
|
|
x=tx, y=ty,
|
|
font_family='sans-serif', font_size=f'{font_size:.3f}px',
|
|
text_anchor='middle', dominant_baseline='hanging',
|
|
stroke=stroke, stroke_width=1,
|
|
stroke_linejoin='round',
|
|
fill=text_color)
|
|
|
|
if title is not None:
|
|
for text_color, stroke in [('white', 'white'), ('black', 'none')]:
|
|
yield Tag('text', children=[title],
|
|
x=w/2, y=title_offset,
|
|
font_family='sans-serif', font_size=f'{font_size:.3f}px',
|
|
text_anchor='middle', dominant_baseline='auto',
|
|
stroke=stroke, stroke_width=1,
|
|
stroke_linejoin='round',
|
|
fill=text_color)
|
|
|
|
for i in range(len(angles)):
|
|
a1, a2 = angles[i]
|
|
a1, a2 = a1%(2*math.pi), a2%(2*math.pi)
|
|
|
|
if a1 > a2:
|
|
angles[i] = a1, 2*math.pi
|
|
angles.append((0, a2))
|
|
|
|
for a1, a2 in angles:
|
|
yield Tag('rect', x=w*math.degrees(a1)/360, y=0, width=w*math.degrees(a2-a1)/360, height=h,
|
|
stroke_width=box_stroke, stroke=color, fill=fill, fill_opacity=0.4)
|
|
|
|
def make_angles(num, w, phase):
|
|
space = (2*math.pi - num*w) / num
|
|
for i in range(num):
|
|
yield (i*(space+w) + phase)%(2*math.pi), (i*(space+w) + w + phase)%(2*math.pi)
|
|
|
|
shift_angles = lambda angles, shift: [((a1+shift)%tau, (a2+shift)%tau) for a1, a2 in angles]
|
|
|
|
angles_1, angles_2 = list(make_angles(n1, mesh_w1, 0)), list(make_angles(n2, mesh_w2, math.radians(phase)))
|
|
plots.append(list(plot_angles(angles_1, c1, 'url(#hatch_1)', title='Mesh 1')))
|
|
plots.append(list(plot_angles(angles_2, c2, 'url(#hatch_2)', title='Mesh 2')))
|
|
|
|
def collide_schedules(sch_a, sch_b):
|
|
boundaries = []
|
|
for a1, a2 in sch_a:
|
|
for b1, b2 in sch_b:
|
|
boundaries.append(((a1-b2 - (b2 - b1))%tau, 1))
|
|
boundaries.append(((a2-b1)%tau, -1))
|
|
|
|
#for a, b in zip(boundaries[0::2], boundaries[1::2]):
|
|
# yield a[0], b[0]
|
|
#return
|
|
|
|
# Prevent floating point rounding errors during sorting affecting the output
|
|
boundaries = [(round(a, 3), d) for a, d in boundaries]
|
|
|
|
prev = None
|
|
rd = sum(1 for a, b in zip(boundaries[0::2], boundaries[1::2]) if b < a)
|
|
boundaries = sorted(boundaries)
|
|
for a, d in boundaries:
|
|
if d == 1 and rd == 0 and prev is not None:
|
|
yield prev, a
|
|
rd += d
|
|
prev = a
|
|
|
|
if rd == 0:
|
|
yield prev, boundaries[0][0]
|
|
|
|
def intersect_schedules(sch_a, sch_b):
|
|
boundaries = []
|
|
for a1, a2 in sch_a + sch_b:
|
|
boundaries.append((a1%tau, 1))
|
|
boundaries.append((a2%tau, -1))
|
|
|
|
# Prevent floating point rounding errors during sorting affecting the output
|
|
boundaries = [(round(a, 3), d) for a, d in boundaries]
|
|
|
|
prev = None
|
|
rd = sum(1 for a, b in zip(boundaries[0::2], boundaries[1::2]) if b < a)
|
|
boundaries = sorted(boundaries)
|
|
for a, d in boundaries:
|
|
if d == -1 and rd == 2 and prev is not None:
|
|
yield prev, a
|
|
rd += d
|
|
prev = a
|
|
|
|
if rd == 2:
|
|
yield prev, boundaries[0][0]
|
|
|
|
print(f'Shifting by {math.degrees(a1):.2f}° and {math.degrees(a2):.2f}°')
|
|
angles_1_intersection_1 = shift_angles(angles_1, -a1)
|
|
angles_1_intersection_2 = shift_angles(angles_1, a1)
|
|
angles_2_intersection_1 = shift_angles(angles_2, -a2)
|
|
angles_2_intersection_2 = shift_angles(angles_2, a2)
|
|
collided_1 = list(collide_schedules(angles_1_intersection_1, angles_2_intersection_1))
|
|
collided_2 = list(collide_schedules(angles_1_intersection_2, angles_2_intersection_2))
|
|
|
|
best_solution = None
|
|
def plot_centers(collided):
|
|
nonlocal best_solution
|
|
|
|
widths = sorted((((end-start)%tau, start, end) for start, end in collided), reverse=True)
|
|
|
|
if collided:
|
|
print('Best phases:')
|
|
w, start, end = widths[0]
|
|
best_solution = (start + (end-start)/2) % tau
|
|
else:
|
|
print('No solution.')
|
|
|
|
for w, start, end in widths:
|
|
if math.isclose(start, end, abs_tol=1e-6):
|
|
print(f'{math.degrees(start):>3.0f}° (exact)')
|
|
else:
|
|
print(f'{math.degrees(start):>5.1f}° - {math.degrees(end):>5.1f}°: phase tolerance {math.degrees(w/2):.2f}°')
|
|
yield Tag('path', stroke=c1, stroke_width=0.2, fill='none',
|
|
d=f'M {100* (((start + w/2) / tau) % 1.0)} 0 v 10')
|
|
|
|
print(' === One-sided schedule === ')
|
|
plots.append([
|
|
*plot_angles(collided_1, 'black', 'url(#hatch_b)', title='Valid shifts for first intersection'),
|
|
*plot_centers(collided_1)
|
|
])
|
|
|
|
print()
|
|
print(' === Two-sided schedule === ')
|
|
intersected = list(intersect_schedules(collided_1, collided_2))
|
|
plots.append([
|
|
*plot_angles(intersected, 'black', 'url(#hatch_b)', title='Valid shifts for both intersections'),
|
|
*plot_centers(intersected)
|
|
])
|
|
|
|
tags2 = tags
|
|
tags = []
|
|
tags.append(Tag('defs', defs))
|
|
dim_tags = []
|
|
|
|
for cx, cy, r, th, sched, stroke, fill in [
|
|
( 0, 0, r1, th1, angles_1, c1, 'url(#hatch_1)'),
|
|
(offset, 0, r2, th2, shift_angles(angles_2, best_solution), c2, 'url(#hatch_2)')]:
|
|
|
|
max_a = max((a2 - a1) % tau for a1, a2 in sched)
|
|
max_w = math.tan(max_a) * (r-th/2)
|
|
max_rd = math.hypot(r + th/2, max_w)
|
|
tags.append(Tag('circle', cx=cx, cy=cy, fill='none', stroke=fill, stroke_opacity=0.4,
|
|
r=((r-th/2) + max_rd)/2, stroke_width=max_rd - (r-th/2)))
|
|
tags.append(Tag('circle', cx=cx, cy=cy, fill='none', stroke=stroke,
|
|
r=(r-th/2), stroke_width=0.15))
|
|
tags.append(Tag('circle', cx=cx, cy=cy, fill='none', stroke=stroke,
|
|
r=max_rd, stroke_width=0.15))
|
|
|
|
group = []
|
|
for a1, a2 in sched:
|
|
da = (a2-a1)%tau
|
|
aw = math.tan(da)*(r-th/2)
|
|
rot = math.degrees(a2 - a1 + a1)
|
|
group.append(Tag('rect', x=cx + r-th/2, y=cy-aw, width=th, height=2*aw,
|
|
fill=stroke, transform=f'rotate({rot} {cx} {cy})'))
|
|
group.append(Tag('animateTransform', attributeName='transform', attributeType='XML', type='rotate',
|
|
_from=f'360 {cx} {cy}', to=f'0 {cx} {cy}', dur='10s', repeatCount='indefinite'))
|
|
tags.append(Tag('g', group))
|
|
print(f'Phase of best solution: {math.degrees(best_solution):.2f}°')
|
|
|
|
plots.append([
|
|
*plot_angles(angles_1_intersection_1, c1, 'url(#hatch_1)', title='First intersection'),
|
|
*plot_angles(shift_angles(angles_2_intersection_1, best_solution), c2, 'url(#hatch_2)', axes=False),
|
|
])
|
|
|
|
plots.append([
|
|
*plot_angles(angles_1_intersection_2, c1, 'url(#hatch_1)', title='Second intersection'),
|
|
Tag('g', [*plot_angles(shift_angles(angles_2_intersection_2, best_solution), c2, 'url(#hatch_2)', axes=False)],
|
|
style='mix-blend-mode:screen'),
|
|
])
|
|
|
|
pitch = 20
|
|
for i, children in enumerate(plots):
|
|
tags2.append(Tag('g', children, transform=f'translate(0 {(i + .2)*pitch})'))
|
|
|
|
out_svg_2.write_text(str(Tag.setup_svg(tags2 + dim_tags, bounds=((0, 0), (100, (i+.8)*pitch)), margin=3)))
|
|
|
|
|
|
out_svg_3.write_text(str(Tag.setup_svg(tags + dim_tags, bounds=(
|
|
(min(-r1, -r2+offset), min(-r1, -r2)),
|
|
(max(r1, r2+offset), max(r1, r2))), margin=3)))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
cli()
|
|
|