496 lines
17 KiB
Python
496 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()
|