goban/lasercut/goban-lasercut.py

497 lines
17 KiB
Python

#!/usr/bin/env python3
#pylint: disable=too-many-instance-attributes
import argparse
import math
from copy import copy
import drawSvg as draw
class Part:
"""
Part of goban
"""
def __init__(self, stones, pos, stonesOffset, config):
self.stones = stones
self.pos = pos
self.stonesOffset = stonesOffset
self.config = config
self.scale = config.resolution
self.size = [
(self.stones[0]-1)*config.cellsize[0],
(self.stones[1]-1)*config.cellsize[1]
]
borders_map = ((3, 1), (0, 2))
total_stones = [
stonesOffset[0] + self.stones[0],
stonesOffset[1] + self.stones[1]
]
self.is_first = [
self.pos[0] == 0,
self.pos[1] == 0
]
self.is_last = [
total_stones[0] == config.size[0],
total_stones[1] == config.size[1]
]
for axis in (0, 1):
if self.is_first[axis]:
self.size[axis] += config.borders[borders_map[axis][0]]
self.size[axis] += config.stone_radius + config.stone_margin
else:
self.size[axis] += config.cellsize[axis]/2
if self.is_last[axis]:
self.size[axis] += config.borders[borders_map[axis][1]]
self.size[axis] += config.stone_radius + config.stone_margin
else:
self.size[axis] += config.cellsize[axis]/2
self.offset = [0, 0]
for axis in (0, 1):
if self.is_first[axis]:
self.offset[axis] += config.borders[borders_map[axis][0]]
self.offset[axis] += config.stone_radius + config.stone_margin
else:
self.offset[axis] += config.cellsize[axis]/2
self.size = [size*self.scale for size in self.size]
self.offset = [offset*self.scale for offset in self.offset]
self.recto_drawing = draw.Drawing(*self.size)
self.verso_drawing = draw.Drawing(*self.size)
self.base_drawing = draw.Drawing(*self.size)
def is_hoshi(self, pos: list):
"""
Is this position a hoshi ?
"""
size = self.config.size
pos = [
pos[0] + self.stonesOffset[0],
pos[1] + self.stonesOffset[1]
]
for axis in (0, 1):
if size[axis] > 10:
if pos[axis] not in (3, round(size[axis]/2)-1, size[axis]-4):
return False
elif pos[axis] not in (2, round(size[axis]/2)-1, size[axis]-3):
return False
return True
def is_engraved(self, pos: list) -> bool:
""" Is the stone at pos engraved ? """
done = self.stonesOffset[0] + pos[0]
done *= self.config.size[0]
done += self.stonesOffset[1] + pos[1]
return done < self.config.engraved_stones
def drawEngraving(self) -> None:
"""
Draw engraving of stones and goban
"""
cellsize = [s*self.scale for s in self.config.cellsize]
radius = self.config.stone_radius * self.scale
margin = self.config.stone_margin * self.scale
thickness = self.config.thickness * self.scale
mark_thickness = self.config.mark_thickness * self.scale
color = self.config.engrave_color
for x in range(self.stones[0]):
for y in range(self.stones[1]):
# pos(recto_x, recto_y, verso_x, verso_y)
pos = (
self.offset[0]+x*cellsize[0],
self.offset[1]+y*cellsize[1],
self.size[0]-self.offset[0]-x*cellsize[0],
self.offset[1]+y*cellsize[1]
)
# Draw grid on recto
if x > 0:
self.recto_drawing.append(
draw.Line(
pos[0]-radius-margin,
pos[1],
pos[0]-cellsize[0]+radius+margin,
pos[1],
stroke=color,
stroke_width=thickness
)
)
if y > 0:
self.recto_drawing.append(
draw.Line(
pos[0],
pos[1]-radius-margin,
pos[0],
pos[1]-cellsize[1]+radius+margin,
stroke=color,
stroke_width=thickness
)
)
if x == 0 and not self.is_first[0]:
self.recto_drawing.append(
draw.Line(
pos[0]-radius-margin,
pos[1],
pos[0]-cellsize[0]/2,
pos[1],
stroke=color,
stroke_width=thickness
)
)
if y == 0 and not self.is_first[1]:
self.recto_drawing.append(
draw.Line(
pos[0],
pos[1]-radius-margin,
pos[0],
pos[1]-cellsize[1]/2,
stroke=color,
stroke_width=thickness
)
)
if x == self.stones[0]-1 and not self.is_last[0]:
self.recto_drawing.append(
draw.Line(
pos[0]+radius+margin,
pos[1],
pos[0]+cellsize[0]/2,
pos[1],
stroke=color,
stroke_width=thickness
)
)
if y == self.stones[1]-1 and not self.is_last[1]:
self.recto_drawing.append(
draw.Line(
pos[0],
pos[1]+radius+margin,
pos[0],
pos[1]+cellsize[1]/2,
stroke=color,
stroke_width=thickness
)
)
# Draw engraving on stones (recto and verso)
if self.is_engraved([x, y]):
engraved_radius = mark_thickness
while engraved_radius < radius:
self.recto_drawing.append(
draw.Circle(
pos[0],
pos[1],
engraved_radius - mark_thickness/2,
fill='none',
stroke=color,
stroke_width=mark_thickness
)
)
self.verso_drawing.append(
draw.Circle(
pos[2],
pos[3],
engraved_radius - mark_thickness/2,
fill='none',
stroke=color,
stroke_width=mark_thickness
)
)
engraved_radius += 2*mark_thickness
# Draw Hoshi on base
if self.is_hoshi((x, y)):
self.base_drawing.append(
draw.Circle(
pos[0],
pos[1],
radius/3,
fill='none',
stroke=color,
stroke_width=thickness
)
)
def drawCutting(self) -> None:
"""
Draw cutting of stones and goban
"""
corner_radius = min(self.config.borders)*self.scale / 2
cellsize = [s*self.scale for s in self.config.cellsize]
radius = self.config.stone_radius * self.scale
margin = self.config.stone_margin * self.scale
thickness = self.config.cut_thickness * self.scale
color = self.config.cut_color
corners = [ # x, y, x, y, start, end
(0, 0, 0, 0, 0, 0),
(self.size[0], 0, self.size[0], 0, 0, 0),
(self.size[0], self.size[1], self.size[0], self.size[1], 0, 0),
(0, self.size[1], 0, self.size[1], 0, 0)
]
# Cut corners on recto and base
for drawing in (self.base_drawing, self.recto_drawing):
if not self.config.no_round_corners:
if self.is_first[0] and self.is_first[1]:
corners[0] = (
corner_radius, 0,
0, corner_radius,
180, 270
)
if self.is_last[0] and self.is_first[1]:
corners[1] = (
self.size[0]-corner_radius, 0,
self.size[0], corner_radius,
270, 0
)
if self.is_first[0] and self.is_last[1]:
corners[3] = (
corner_radius, self.size[1],
0, self.size[1]-corner_radius,
90, 180
)
if self.is_last[0] and self.is_last[1]:
corners[2] = (
self.size[0]-corner_radius, self.size[1],
self.size[0], self.size[1]-corner_radius,
0, 90
)
# Cut perimeter
for (idx, corner) in enumerate(corners):
for drawing in (self.base_drawing, self.recto_drawing):
axis = 2 if idx%2==0 else 0
drawing.append(
draw.Line(
corners[idx-1][0+axis],
corners[idx-1][1+axis],
corner[0+axis],
corner[1+axis],
stroke=color,
stroke_width=thickness
)
)
if corner[4] != corner[5]:
drawing.append(
draw.Arc(
corner[0],
corner[3],
corner_radius,
corner[4],
corner[5],
stroke=color,
stroke_width=thickness,
fill='none'
)
)
# Cut stones on recto and holes on base
for x in range(self.stones[0]):
for y in range(self.stones[1]):
# pos(recto_x, recto_y)
pos = (
self.offset[0]+x*cellsize[0],
self.offset[1]+y*cellsize[1],
self.size[0]-self.offset[0]-x*cellsize[0],
self.offset[1]+y*cellsize[1]
)
self.recto_drawing.append(
draw.Circle(
pos[0],
pos[1],
radius+margin,
fill='none',
stroke=color,
stroke_width=thickness
)
)
self.recto_drawing.append(
draw.Arc(
pos[0],
pos[1],
radius,
90+math.acos(0.75)*360/(2*math.pi),
90-math.acos(0.75)*360/(2*math.pi),
fill='none',
stroke=color,
stroke_width=thickness
)
)
self.recto_drawing.append(
draw.Arc(
pos[0],
pos[1]+3*radius/2,
radius,
270-math.acos(0.75)*360/(2*math.pi),
270+math.acos(0.75)*360/(2*math.pi),
fill='none',
stroke=color,
stroke_width=thickness
)
)
self.base_drawing.append(
draw.Circle(
pos[0],
pos[1],
radius/4,
fill='none',
stroke=color,
stroke_width=thickness
)
)
def save(self) -> None:
"""
Save drawing.
"""
prefix = self.config.prefix
suffix = f'-{self.pos[0]}-{self.pos[1]}'
self.recto_drawing.saveSvg(prefix+'recto'+suffix+'.svg')
self.verso_drawing.saveSvg(prefix+'verso'+suffix+'.svg')
self.base_drawing.saveSvg(prefix+'base'+suffix+'.svg')
class GobanDraw:
"""
Drawing a goban and its stones.
@param config: Config from argparse
"""
def __init__(self, config) -> None:
self.config = config
self.parts = []
stones = list(config.size)
part_stones = [0, 0]
offset = [0, 0]
x = 0
y = 0
while stones[0] > 0:
stones[1] = config.size[1]
offset[1] = 0
y = 0
while stones[1] > 0:
part_stones = (
min(config.max_part_size[0], config.size[0]-offset[0]),
min(config.max_part_size[1], config.size[1]-offset[1])
)
self.parts.append(
Part(
part_stones,
(x, y),
copy(offset),
config
)
)
offset[1] += part_stones[1]
stones[1] -= part_stones[1]
y += 1
offset[0] += part_stones[0]
stones[0] -= part_stones[0]
x += 1
for part in self.parts:
print(f"Part size: {part.size[0]}x{part.size[1]}")
if not self.config.no_engrave:
part.drawEngraving()
if not self.config.no_cut:
part.drawCutting()
def save(self):
""" Save all sketches """
for part in self.parts:
part.save()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Create goban and stones for lasercut"
)
parser.add_argument(
'--size', type=int, nargs=2, default=(19, 19),
help="Size of goban (number of stones)", metavar=('X', 'Y')
)
parser.add_argument(
'--max-part-size', type=int, nargs=2, default=(19, 19),
help="Max size of part of goban (number of stones)", metavar=('X', 'Y')
)
parser.add_argument(
'--cellsize', type=float, nargs=2, default=(15, 15),
help="Size of one cell in mm", metavar=('X', 'Y')
)
parser.add_argument(
'--stone-radius', type=float, default=6.5,
help="Radius of stone in mm"
)
parser.add_argument(
'--stone-margin', type=float, default=0,
help="Margin of stone in mm"
)
parser.add_argument(
'--borders', type=float, nargs=4, default=(5, 5, 5, 5),
help="Borders of goban in mm",
metavar=('UP', 'RIGHT', 'BOTTOM', 'LEFT')
)
parser.add_argument(
'--thickness', type=float, default=2.0,
help="Thickness of grid in mm"
)
parser.add_argument(
'--mark-thickness', type=float, default=1.5,
help="Thickness of mark on stone in mm"
)
parser.add_argument(
'--cut-thickness', type=float, default=0.2,
help="Thickness of cut stroke in mm"
)
parser.add_argument(
'--no-engrave', default=False, action='store_true',
help='Disable engraving'
)
parser.add_argument(
'--no-cut', default=False, action='store_true',
help='Disable cutting'
)
parser.add_argument(
'--no-round-corners', default=False, action='store_true',
help='No round corners for goban'
)
parser.add_argument(
'--engrave-color', type=str, default='#000000',
help="Engrave color in web format #000000 to #ffffff"
)
parser.add_argument(
'--cut-color', type=str, default='#ff0000',
help="Cut color in web format #000000 to #ffffff"
)
parser.add_argument(
'--engraved-stones', type=int, default=181,
help="Number of engraved (black) stones"
)
parser.add_argument(
'--prefix', default='',
help='Prefix for filenames, ' +
'create three files named prefixrecto.svg, prefixverso.svg and '+
'prefixbase.svg'
)
parser.add_argument(
'--resolution', type=int, default=100,
help="Resolution in pixels/mm"
)
conf = parser.parse_args()
goban = GobanDraw(conf)
goban.save()