#!/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()