Compare commits

...

10 Commits

Author SHA1 Message Date
Matt McWilliams 89ed6011fc Lock requirements from pip freeze on a working system with python 3.8 2023-12-16 10:30:18 -05:00
Michael Fogleman a5a12f0163 fractal 2019-11-26 15:31:29 -05:00
Michael Fogleman bb32105061 overlapping circles 2019-11-23 11:19:18 -05:00
Michael Fogleman fbf7f5bdca handibot 2019-10-19 16:38:04 -04:00
Michael Fogleman 1569f75fd1 circles offset 2019-10-03 20:07:12 -04:00
Michael Fogleman aa12c04186 nes updates 2018-08-19 16:49:32 -04:00
Michael Fogleman 3ab0e05d2f .axi file with comments 2018-08-19 16:49:19 -04:00
Michael Fogleman 8a8c98a113 rush 2018-07-24 08:54:09 -04:00
Michael Fogleman e65e935f7b rush 2018-07-23 18:02:22 -04:00
Michael Fogleman 84cb2f5b32 box 2018-04-11 13:03:10 -04:00
10 changed files with 610 additions and 30 deletions

View File

@ -32,7 +32,10 @@ class Drawing(object):
def loads(cls, data):
paths = []
for line in data.split('\n'):
path = line.strip().split()
line = line.strip()
if line.startswith('#'):
continue
path = line.split()
path = [tuple(map(float, x.split(','))) for x in path]
path = expand_quadratics(path)
if path:

16
examples/box.py Normal file
View File

@ -0,0 +1,16 @@
import axi
W, H = 14, 11
BOUNDS = axi.A3_BOUNDS
def main():
paths = [
[(0, 0), (W, 0), (W, H), (0, H), (0, 0)]
]
d = axi.Drawing(paths)
d = d.center(*BOUNDS[-2:])
d.dump('box.axi')
d.render(bounds=BOUNDS).write_to_png('box.png')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,56 @@
import axi
import math
import random
W, H = axi.A3_SIZE
def circle_ray_intersection(cx, cy, cr, ox, oy, dx, dy):
xd = ox - cx
yd = oy - cy
a = dx * dx + dy * dy
b = 2 * (dx * (ox - cx) + dy * (oy - cy))
c = xd * xd + yd * yd - cr * cr
d = b * b - 4 * a * c
if d < 0:
return None
t = (-b + math.sqrt(d)) / (2 * a)
x = ox + dx * t
y = oy + dy * t
return (x, y)
def path(x0, y0, r0, x1, y1, r1):
t = random.random()
a0 = random.random() * 2 * math.pi
a1 = a0 + math.radians(10)
n = 100
result = []
for i in range(n + 1):
u = i / n
a = a0 + (a1 - a0) * u
dx = math.cos(a)
dy = math.sin(a)
ax, ay = circle_ray_intersection(x0, y0, r0, x0, y0, dx, dy)
bx, by = circle_ray_intersection(x1, y1, r1, x0, y0, dx, dy)
x = ax + (bx - ax) * t
y = ay + (by - ay) * t
result.append((x, y))
return result
def main():
x0 = 0
y0 = 0
r0 = 1.5
x1 = -0.25
y1 = 0
r1 = 2
paths = []
for i in range(500):
paths.append(path(x0, y0, r0, x1, y1, r1))
d = axi.Drawing(paths).rotate_and_scale_to_fit(W, H)#.sort_paths()
im = d.render()
im.write_to_png('circles_offset.png')
d.dump('circles_offset.axi')
# axi.draw(d)
if __name__ == '__main__':
main()

28
examples/fractal.py Normal file
View File

@ -0,0 +1,28 @@
from shapely import geometry
import axi
import sys
def main():
size = axi.A3_SIZE
bounds = axi.A3_BOUNDS
d = axi.Drawing.load(sys.argv[1])
print(len(d.paths[0]))
d = d.scale_to_fit(*size).center(*size)
d = d.simplify_paths(0.01 / 25.4)
print(len(d.paths[0]))
g = geometry.Polygon(d.paths[0])
while True:
b = -0.25 / 25.4
g = g.buffer(b)
if g.is_empty:
break
g = g.simplify(0.01 / 25.4)
d.paths.extend(axi.shapely_to_paths(g))
print(d.bounds)
d.dump('out.axi')
d.render(bounds=bounds, line_width=0.2/25.4).write_to_png('out.png')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,75 @@
from math import *
from shapely import geometry, ops
import axi
W = 6
H = 8
BOUNDS = (0, 0, W, H)
BIT_RADIUS = 0.125
def regular_polygon(n, x, y, r):
points = []
for i in range(n):
t = 2 * pi / n * i
points.append((x + r * cos(t), y + r * sin(t)))
points.append(points[0])
return points
def polygon_splits(n, x, y, r, b):
lines = []
for i in range(n):
t = 2 * pi / n * (i + 0.5)
lines.append([(x, y), (x + r * cos(t), y + r * sin(t))])
return geometry.MultiLineString(lines).buffer(b)
def polygon(n, r, br, notch=False):
p = regular_polygon(n, 0, 0, r)
g = geometry.Polygon(p)
g = g.buffer(br).exterior
if notch:
g = g.difference(polygon_splits(n, 0, 0, r * 2, br * 2))
g = ops.linemerge(g)
p = axi.shapely_to_paths(g)
return axi.Drawing(p).origin()
def drawings_to_gcode(ds, zs, z0, f):
lines = []
lines.append('G90') # absolute coordinates
lines.append('G20') # inches
lines.append('G0 Z%g' % z0) # jog to z0
lines.append('M4') # turn on router
lines.append('G4 P2.0') # dwell for N seconds
lines.append('F%g' % f) # set feed rate (inches per minute)
for d, z in zip(ds, zs):
for path in d.paths:
# jog to first point
x, y = path[0]
lines.append('G0 X%g Y%g' % (x, y))
# move z down
lines.append('G1 Z%g' % z)
# draw path
for x, y in path[1:]:
lines.append('G1 X%g Y%g' % (x, y))
# move z up
lines.append('G1 Z%g' % z0)
lines.append('M8')
lines.append('G0 X0 Y0')
return '\n'.join(lines)
def main():
d0 = polygon(3, 3 / sqrt(3), BIT_RADIUS, False)
d1 = polygon(3, 3 / sqrt(3), BIT_RADIUS, True)
ds = [d0, d0, d1]
ds = [d.translate(1, 1) for d in ds]
zs = [-0.25, -0.5, -0.75]
g = drawings_to_gcode(ds, zs, 0.5, 30)
print(g)
for i, (d, z) in enumerate(zip(ds, zs)):
d.render(bounds=BOUNDS).write_to_png('z%g.png' % z)
if __name__ == '__main__':
main()

View File

@ -1,20 +1,24 @@
from __future__ import division
from __future__ import division, print_function
import axi
import numpy as np
import os
import sys
NUMBER = '?'
TITLE = 'Five Seconds of Donkey Kong'
W, H = 11-2, 14-2
DW, DH = axi.A3_SIZE
NUMBER = '48'
TITLE = 'Fifteen Seconds of The Legend of Zelda'
LABEL = '#%s' % NUMBER
COLUMNS = 6
SECONDS = 5
FRAME_OFFSET = 0
MIN_CHANGES = 1
COLUMNS = 8
SECONDS = 15
FRAME_OFFSET = 900
MIN_CHANGES = 2
UNIQUE = False
SIMPLIFY = 0
SIMPLIFY = 5
def simplify_sparkline(values, n):
if not n:
@ -52,11 +56,11 @@ def title():
d = d.join_paths(0.01)
return d
def label():
def label(x, y):
d = axi.Drawing(axi.text(LABEL, axi.FUTURAL))
d = d.scale_to_fit_height(0.125)
d = d.rotate(-90)
d = d.move(12, 8.5, 1, 1)
d = d.move(x, y, 1, 1)
d = d.join_paths(0.01)
return d
@ -72,9 +76,9 @@ def main():
lines = filter(None, lines)
# read values and transpose
data = [map(int, line.split(',')) for line in lines]
data = [tuple(map(int, line.split(','))) for line in lines]
data = np.transpose(data)
print '%d series in file' % len(data)
print('%d series in file' % len(data))
# trim to SECONDS worth of data
n = len(data[0])
@ -86,7 +90,7 @@ def main():
# remove addresses with too few values
data = [x for x in data if len(set(x)) > MIN_CHANGES]
print '%d series that changed' % len(data)
print('%d series that changed' % len(data))
# remove duplicate series
if UNIQUE:
@ -99,16 +103,16 @@ def main():
seen.add(k)
new_data.append(x)
data = new_data
print '%d unique series' % len(data)
print('%d unique series' % len(data))
# delete repetitive stuff
# del data[136:136+8*14]
del data[136:136+8*14]
# trim so all rows are full
data = data[:int((len(data) // COLUMNS) * COLUMNS)]
print '%d series after trimming' % len(data)
print('%d series after trimming' % len(data))
print '%d data points each' % len(data[0])
print('%d data points each' % len(data[0]))
# create sparklines in a grid pattern
paths = []
@ -128,16 +132,21 @@ def main():
y = 1 - value + r * 1.5
path.append((x, y))
paths.append(path)
d = axi.Drawing(paths)
# add title and label and fit to page
d = d.scale(8.5 / d.width, (12 - 0.5) / d.height)
d = d.scale(W / d.width, (H - 0.5) / d.height)
d = stack_drawings([d, title()], 0.25)
d = d.rotate(-90)
d = d.center(12, 8.5)
d.add(label())
d = d.center(DW, DH)
_, _, lx, ly = d.bounds
d.add(label(lx, ly))
print d.bounds
d = d.simplify_paths(0.001)
print(d.bounds)
print(d.size)
# save outputs
dirname = 'nes/%s' % NUMBER
@ -146,11 +155,10 @@ def main():
except Exception:
pass
d.dump(os.path.join(dirname, 'out.axi'))
rotated = d.rotate(90).center(8.5, 12)
rotated = d.rotate(90).center(DH, DW)
rotated.dump_svg(os.path.join(dirname, 'out.svg'))
im = rotated.render(
scale=109 * 1, line_width=0.3/25.4,
show_axi_bounds=False, use_axi_bounds=False)
x0, y0, x1, y1 = rotated.bounds
im = rotated.render(bounds=(x0 - 1, y0 - 1, x1 + 1, y1 + 1))
im.write_to_png(os.path.join(dirname, 'out.png'))
if __name__ == '__main__':

View File

@ -0,0 +1,191 @@
package main
import (
"fmt"
"math"
"os"
"sort"
)
const (
// D = math.Pi
S = 2
)
type Point struct {
X, Y float64
}
func (a Point) Lerp(b Point, t float64) Point {
x := a.X + (b.X-a.X)*t
y := a.Y + (b.Y-a.Y)*t
return Point{x, y}
}
type Segment struct {
P0, P1 Point
}
type Circle struct {
X, Y, R float64
}
func (c Circle) ContainsPoint(p Point) bool {
return math.Hypot(p.X-c.X, p.Y-c.Y) < c.R
}
func (c Circle) Discretize(n int) []Point {
points := make([]Point, n)
for i := range points {
t := float64(i) / float64(n-1)
a := 2 * math.Pi * t
x := math.Cos(a)*c.R + c.X
y := math.Sin(a)*c.R + c.Y
points[i] = Point{x, y}
}
return points
}
func (c Circle) IntersectLine(p0, p1 Point) (float64, float64, bool) {
dx := p1.X - p0.X
dy := p1.Y - p0.Y
A := dx*dx + dy*dy
B := 2 * (dx*(p0.X-c.X) + dy*(p0.Y-c.Y))
C := (p0.X-c.X)*(p0.X-c.X) + (p0.Y-c.Y)*(p0.Y-c.Y) - c.R*c.R
det := B*B - 4*A*C
if A <= 0 || det <= 0 {
return 0, 0, false
}
t0 := (-B + math.Sqrt(det)) / (2 * A)
t1 := (-B - math.Sqrt(det)) / (2 * A)
return t0, t1, true
}
func makeCircles(circleRadius, visibleRadius float64) []Circle {
var circles []Circle
a := int(math.Ceil(circleRadius + visibleRadius))
for y := -a; y <= a; y++ {
for x := -a; x <= a; x++ {
cx := float64(x)
cy := float64(y)
if math.Hypot(cx, cy) <= circleRadius+visibleRadius {
circles = append(circles, Circle{cx, cy, circleRadius})
}
}
}
return circles
}
func count(circles []Circle, p Point) int {
var result int
for _, c := range circles {
if c.ContainsPoint(p) {
result++
}
}
return result
}
type splitFunc func(Point) bool
func split(circles []Circle, p0, p1 Point, f splitFunc) []Segment {
var ts []float64
for _, c := range circles {
t0, t1, ok := c.IntersectLine(p0, p1)
if ok {
ts = append(ts, t0)
ts = append(ts, t1)
}
}
sort.Float64s(ts)
var segments []Segment
for i := 1; i < len(ts); i++ {
t0 := ts[i-1]
t1 := ts[i]
if t1 < 0 || t0 > 1 {
continue
}
t0 = math.Max(t0, 0)
t1 = math.Min(t1, 1)
t := (t0 + t1) / 2
p := p0.Lerp(p1, t)
if f(p) {
q0 := p0.Lerp(p1, t0)
q1 := p0.Lerp(p1, t1)
segments = append(segments, Segment{q0, q1})
}
}
return segments
}
func run(path string, d, s float64) error {
file, err := os.Create(path)
if err != nil {
return err
}
circles := makeCircles(d/2, s*math.Sqrt(2))
x0 := -s
x1 := s
y0 := -s
y1 := s
f := func(p Point) bool {
return count(circles, p)%2 == 1
}
g := func(p Point) bool {
return math.Hypot(p.X, p.Y) <= s
}
outer := []Circle{Circle{0, 0, s}}
const n = 200
for i := 0; i <= n; i++ {
t := float64(i) / float64(n)
y := y0 + (y1-y0)*t
p0 := Point{x0, y}
p1 := Point{x1, y}
segments := split(circles, p0, p1, f)
for _, s := range segments {
clipped := split(outer, s.P0, s.P1, g)
for _, cs := range clipped {
fmt.Fprintf(file, "%g,%g %g,%g\n", cs.P0.X, cs.P0.Y, cs.P1.X, cs.P1.Y)
}
}
}
for _, c := range circles {
points := c.Discretize(360)
for i := 1; i < len(points); i++ {
p0 := points[i-1]
p1 := points[i]
clipped := split(outer, p0, p1, g)
for _, cs := range clipped {
fmt.Fprintf(file, "%g,%g %g,%g\n", cs.P0.X, cs.P0.Y, cs.P1.X, cs.P1.Y)
}
}
}
points := outer[0].Discretize(360)
for _, p := range points {
fmt.Fprintf(file, "%g,%g ", p.X, p.Y)
}
fmt.Fprintf(file, "\n")
return nil
}
func main() {
d0 := 1.0
d1 := math.Pi
n := 48
for i := 0; i < n; i++ {
t := float64(i) / float64(n-1)
d := d0 + (d1-d0)*t
path := fmt.Sprintf("overlapping_circles/%.8f.axi", d)
fmt.Println(path)
run(path, d, S)
}
}

View File

@ -0,0 +1,48 @@
import axi
import os
N_PER_ROW = 8
SPACING = 2
def load(path):
d = axi.Drawing.load(path)
d = d.scale_to_fit(1.9, 1.9)
d = d.join_paths(0.5 / 25.4)
d = d.sort_paths()
d = d.join_paths(0.5 / 25.4)
d = d.origin()
return d
def main():
dirname = 'overlapping_circles'
i = 0
j = 0
x = 0
y = 0
drawing = axi.Drawing([])
for filename in sorted(os.listdir(dirname)):
if not filename.endswith('.axi'):
continue
path = os.path.join(dirname, filename)
print(path)
d = load(path)
d = d.translate(x, y)
drawing.add(d)
x += SPACING
i += 1
if i == N_PER_ROW:
i = 0
j += 1
x = 0
if j % 2:
x = SPACING / 2
y += SPACING * 0.866
d = drawing
d = d.center(*axi.A3_SIZE)
print(len(d.paths))
im = d.render(bounds=axi.A3_BOUNDS, line_width = 0.4 / 25.4)
im.write_to_png('overlapping_circles.png')
d.dump('overlapping_circles.axi')
if __name__ == '__main__':
main()

152
examples/rush.py Normal file
View File

@ -0,0 +1,152 @@
from collections import *
from math import *
import axi
import fileinput
BOUNDS = axi.A3_BOUNDS
X, Y, W, H = BOUNDS
P = 0.25
R = 0.125
COLS = 16
ROWS = 16
N = ROWS * COLS
def rectangle(x, y, w, h):
return [
(x, y),
(x + w, y),
(x + w, y + h),
(x, y + h),
(x, y),
]
def padded_rectangle(x, y, w, h, p):
x += p
y += p
w -= p * 2
h -= p * 2
return rectangle(x, y, w, h)
def arc(cx, cy, r, a0, a1, n):
path = []
for i in range(n+1):
t = i / n
a = a0 + (a1 - a0) * t
x = cx + r * cos(a)
y = cy + r * sin(a)
path.append((x, y))
return path
def rounded_rectangle(x, y, w, h, r):
n = 18
x0, x1, x2, x3 = x, x + r, x + w - r, x + w
y0, y1, y2, y3 = y, y + r, y + h - r, y + h
path = []
path.extend([(x1, y0), (x2, y0)])
path.extend(arc(x2, y1, r, radians(270), radians(360), n))
path.extend([(x3, y1), (x3, y2)])
path.extend(arc(x2, y2, r, radians(0), radians(90), n))
path.extend([(x2, y3), (x1, y3)])
path.extend(arc(x1, y2, r, radians(90), radians(180), n))
path.extend([(x0, y2), (x0, y1)])
path.extend(arc(x1, y1, r, radians(180), radians(270), n))
return path
def padded_rounded_rectangle(x, y, w, h, r, p):
x += p
y += p
w -= p * 2
h -= p * 2
return rounded_rectangle(x, y, w, h, r)
def wall(x, y):
return [arc(x+0.5, y+0.5, 0.333, 0, 2*pi, 72)]
x0 = x + P
y0 = y + P
x1 = x + 1 - P
y1 = y + 1 - P
paths = [rectangle(x0, y0, x1 - x0, y1 - y0)]
paths.append([(x0, y0), (x1, y1)])
paths.append([(x0, y1), (x1, y0)])
return paths
def xy(i):
x = i % 6
y = i // 6
return (x, y)
def desc_paths(desc):
paths = []
lookup = defaultdict(list)
for i, c in enumerate(desc):
lookup[c].append(i)
for c in sorted(lookup):
ps = lookup[c]
if c == 'o':
continue
elif c == 'x':
for i in ps:
x, y = xy(i)
paths.extend(wall(x, y))
else:
stride = ps[1] - ps[0]
i0 = ps[0]
i1 = ps[-1]
x0, y0 = xy(i0)
x1, y1 = xy(i1)
dx = x1 - x0
dy = y1 - y0
# paths.append(padded_rounded_rectangle(x0, y0, dx + 1, dy + 1, R, P))
if c == 'A':
paths.append(padded_rectangle(x0, y0, dx + 1, dy + 1, 0.25))
else:
paths.append([(x0 + 0.5, y0 + 0.5), (x0 + dx + 0.5, y0 + dy + 0.5)])
# if c == 'A':
# if stride > 1:
if len(ps) == 3:
# if stride == 1:
# if len(ps) == 2:
# if False:
paths.append(padded_rectangle(x0, y0, dx + 1, dy + 1, 0.35))
# s = 0.1
# p = P + s
# while p < 0.5:
# paths.append(padded_rounded_rectangle(x0, y0, dx + 1, dy + 1, R, p))
# p += s
return paths
def main():
drawing = axi.Drawing()
font = axi.Font(axi.FUTURAL, 12)
n = 0
for line in fileinput.input():
fields = line.strip().split()
desc = fields[1]
moves = int(fields[0])
# if 'x' in desc:
# continue
paths = desc_paths(desc)
d = axi.Drawing(paths)
i = n % COLS
j = n // COLS
d = d.translate(i * 8, j * 10)
drawing.add(d)
d = font.wrap(str(moves), 10)
d = font.wrap(bin(moves)[2:].replace('1', '\\').replace('0', '/'), 10)
# d = d.scale(0.1, 0.1)
d = d.scale_to_fit_height(1)
d = d.move(i * 8 + 3, j * 10 + 6.5, 0.5, 0)
drawing.add(d)
n += 1
if n == N:
break
# d = axi.Drawing(paths)
d = drawing
d = d.rotate_and_scale_to_fit(W, H, step=90)
d.dump('rush.axi')
d.render(bounds=None, show_bounds=False, scale=300).write_to_png('rush.png')
if __name__ == '__main__':
main()

View File

@ -1,4 +1,7 @@
cairocffi
pyserial
pyhull
Shapely
cairocffi==1.2.0
cffi==1.14.4
numpy==1.19.5
pycparser==2.20
pyhull==2015.2.1
pyserial==3.5
Shapely==1.7.1