From 16e60427d10866277ced518e42d380a3a3e29f89 Mon Sep 17 00:00:00 2001
From: Matthias Neeracher <microtherion@gmail.com>
Date: Sun, 14 Nov 2021 06:07:31 +0100
Subject: [PATCH] Pincushion experiment

---
 pincushion.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 93 insertions(+)
 create mode 100755 pincushion.py

diff --git a/pincushion.py b/pincushion.py
new file mode 100755
index 0000000..0db5558
--- /dev/null
+++ b/pincushion.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+
+import math
+import sys
+
+M=2
+N=5
+L=20.0
+B=2.5
+
+def frange(*args):
+    """frange([start, ] end [, step [, mode]]) -> generator
+
+    A float range generator. If not specified, the default start is 0.0
+    and the default step is 1.0.
+
+    Optional argument mode sets whether frange outputs an open or closed
+    interval. mode must be an int. Bit zero of mode controls whether start is
+    included (on) or excluded (off); bit one does the same for end. Hence:
+
+        0 -> open interval (start and end both excluded)
+        1 -> half-open (start included, end excluded)
+        2 -> half open (start excluded, end included)
+        3 -> closed (start and end both included)
+
+    By default, mode=1 and only start is included in the output.
+    """
+    mode = 1  # Default mode is half-open.
+    n = len(args)
+    if n == 1:
+        args = (0.0, args[0], 1.0)
+    elif n == 2:
+        args = args + (1.0,)
+    elif n == 4:
+        mode = args[3]
+        args = args[0:3]
+    elif n != 3:
+        raise TypeError('frange expects 1-4 arguments, got %d' % n)
+    assert len(args) == 3
+    try:
+        start, end, step = [a + 0.0 for a in args]
+    except TypeError:
+        raise TypeError('arguments must be numbers')
+    if step == 0.0:
+        raise ValueError('step must not be zero')
+    if not isinstance(mode, int):
+        raise TypeError('mode must be an int')
+    if mode & 1:
+        i, x = 0, start
+    else:
+        i, x = 1, start+step
+    if step > 0:
+        if mode & 2:
+            from operator import le as comp
+        else:
+            from operator import lt as comp
+    else:
+        if mode & 2:
+            from operator import ge as comp
+        else:
+            from operator import gt as comp
+    while comp(x, end):
+        yield x
+        i += 1
+        x = start + i*step
+
+def cushion(x0, y0, hole, space):
+    yoff  = .866*space # sqrt(3)/2
+    xodd  = list(frange(x0+B, x0+L-B, space, 3))
+    xeven = list(frange(x0+B+.5*space, x0+L-B, space, 3))
+    row   = 0
+    for y in frange(y0+B, y0+L-B, yoff, 3):
+        row += 1
+        if (row & 1) == 1:
+            xrow = xodd
+        else:
+            xrow = xeven
+        for x in xrow:
+            print('<circle cx="{}" cy="{}" r="{}" stroke="black" fill="none"/>'.format(x, y, hole))
+
+
+print('<svg viewBox="-1 -1 101 41" width="102mm" height="42mm" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg">')
+print('<rect height="{}" width="{}" stroke="black" fill="none"/>'.format(M*L, N*L))
+for i in range(1,M):
+    print('<line x1="0" y1="{}" x2="{}" y2="{}" stroke="red"/>'.format(i*L, N*L, i*L))
+for j in range(1,N):
+    print('<line x1="{}" y1="0" x2="{}" y2="{}" stroke="red"/>'.format(j*L, j*L, M*L))
+HOLE = [0.1, 0.05]
+SPACE = [2.0, 1.5, 1.0, 0.75, 0.5]
+for i in range(M):
+    for j in range(N):
+        cushion(j*L, i*L, HOLE[i], SPACE[j])
+print('</svg>')