# Copyright (C) 2016 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import io
from pathlib import Path
import boxes
from boxes import *
from boxes import lids
class TrayLayoutFile(Boxes):
"""Generate a layout file for a typetray."""
# This class generates the skeleton text file that can then be edited
# to describe the actual box
description = """This is a two step process. This is step 1.
The layout is based on a grid of sizes in x and y direction.
Choose how many distances you need in both directions.
The actual sizes and all other settings can be entered in the second step."""
webinterface = False
ui_group = "Tray"
sx: list[float] = [] # arg input
sy: list[float] = [] # arg input
hwalls: list[list[bool]] = []
vwalls: list[list[bool]] = []
floors: list[list[bool]] = []
def __init__(self, input=None, webargs=False) -> None:
Boxes.__init__(self)
self.argparser = argparse.ArgumentParser()
self.buildArgParser("sx", "sy")
self.argparser.add_argument(
"--output", action="store", type=str, default="traylayout.txt",
help="Name of the layout text file.")
def open(self) -> None:
# Use empty open and close methods to avoid initializing the whole drawing infrastructure.
pass
def close(self):
# Use empty open and close methods to avoid initializing the whole drawing infrastructure.
return io.BytesIO(bytes(str(self), 'utf-8'))
def fillDefault(self, sx: list[float], sy: list[float]) -> None:
self.sx = sx
self.sy = sy
x = len(sx)
y = len(sy)
self.hwalls = [[True for _ in range(x)] for _ in range(y + 1)]
self.vwalls = [[True for _ in range(x + 1)] for _ in range(y)]
self.floors = [[True for _ in range(x)] for _ in range(y)]
def __str__(self) -> str:
r = []
for i, x in enumerate(self.sx):
r.append(" |" * i + " ,> %.1fmm\n" % x)
for hwalls, vwalls, floors, y in zip(self.hwalls, self.vwalls, self.floors, self.sy):
r.append("".join("+" + " -"[h] for h in hwalls) + "+\n")
r.append("".join((" |"[v] + "X "[f] for v, f in zip(vwalls, floors))) + " |"[vwalls[-1]] + " %.1fmm\n" % y)
r.append("".join("+" + " -"[h] for h in self.hwalls[-1]) + "+\n")
return "".join(r)
def render(self) -> None:
self.fillDefault(self.sx, self.sy)
[docs]
class TrayLayout(Boxes):
"""Generate a typetray from a layout file."""
# This class reads in the layout either from a file (with --input) or
# as string (with --layout) and turns it into a drawing for a box.
ui_group = "Tray"
description = """This is a two step process. This is step 2.
Edit the layout text graphics to adjust your tray.
Put in the sizes for each column and row. You can replace the hyphens and
vertical bars representing the walls with a space character to remove the walls.
You can replace the space characters representing the floor by a "X" to remove the floor for this compartment.
"""
def __init__(self) -> None:
super().__init__()
self.addSettingsArgs(boxes.edges.FingerJointSettings)
self.addSettingsArgs(lids.LidSettings)
self.buildArgParser("h", "hi", "outside", "sx", "sy")
self.argparser.add_argument(
"--layout", action="store", type=str, default="\n",
help="""* Set **sx** and **sy** before editing this!
* You can still change measurements afterwards
* You can replace the hyphens and vertical bars representing the walls
with a space character to remove the walls.
* You can replace the space characters representing the floor by a "X"
to remove the floor for this compartment.
* Resize text area if necessary.""")
self.description = ""
if self.UI != "web":
self.argparser.add_argument(
"--input", action="store", type=str,
default="traylayout.txt",
help="layout file")
def vWalls(self, x: int, y: int) -> int:
"""Number of vertical walls at a crossing."""
result = 0
if y > 0 and self.vwalls[y - 1][x]:
result += 1
if y < len(self.y) and self.vwalls[y][x]:
result += 1
return result
def hWalls(self, x: int, y: int) -> int:
"""Number of horizontal walls at a crossing."""
result = 0
if x > 0 and self.hwalls[y][x - 1]:
result += 1
if x < len(self.x) and self.hwalls[y][x]:
result += 1
return result
def vFloor(self, x: int, y: int) -> bool:
"""Is there floor under vertical wall."""
if y >= len(self.y):
return False
return (x > 0 and self.floors[y][x - 1]) or (x < len(self.x) and self.floors[y][x])
def hFloor(self, x: int, y: int) -> bool:
"""Is there floor under horizontal wall."""
if x >= len(self.x):
return False
return (y > 0 and self.floors[y - 1][x]) or (y < len(self.y) and self.floors[y][x])
@restore
def edgeAt(self, edge, x, y, length, angle=0):
self.moveTo(x, y, angle)
edge = self.edges.get(edge, edge)
edge(length)
@restore
def cornerAt(self, x, y, length, angle=0):
self.moveTo(x, y, angle)
self.polyline(length, 90, length)
def prepare(self):
if self.layout:
self.parse(self.layout.split('\n'))
elif Path(self.input).exists():
with Path(self.input).open() as input_file:
self.parse(input_file)
elif callable(getattr(self, "generate_layout", None)):
self.parse(self.generate_layout().split('\n'))
else:
raise RuntimeError("traylayout requires --layout, --input, or implementation of generate_layout")
if self.outside:
self.x = self.adjustSize(self.x)
self.y = self.adjustSize(self.y)
self.h = self.adjustSize(self.h, e2=False)
if self.hi:
self.hi = self.adjustSize(self.hi, e2=False)
self.hi = self.hi or self.h
self.edges["s"] = boxes.edges.Slot(self, self.hi / 2.0)
self.edges["C"] = boxes.edges.CrossingFingerHoleEdge(self, self.hi)
self.edges["D"] = boxes.edges.CrossingFingerHoleEdge(self, self.hi, outset=self.thickness)
def wallLabelsCB(self, start, end, row, x=True):
if not self.labels:
return
sx = self.x if x else self.y
posx = 0
for pos in range(start, end):
posx += sx[pos] / 2
self.text(f"x {pos+1}/{row+1}" if x else f"y {row+1}/{pos+1}", posx, 0, color=Color.ANNOTATIONS, align="center", fontsize=2*self.thickness)
posx += sx[pos] / 2 + self.thickness
def walls(self, move=None):
lx = len(self.x)
ly = len(self.y)
le_f = re_f = ole_f = ore_f = "f"
le_F = re_F = ole_F = ore_F = "F"
if self.hi > self.h:
# if hi is bigger limit finger joints at the outside to h
le_f = boxes.edges.CompoundEdge(self, "ef", [self.hi-self.h, self.h])
re_f = boxes.edges.CompoundEdge(self, "fe", [self.h, self.hi-self.h])
le_F = boxes.edges.CompoundEdge(self, "eF", [self.hi-self.h, self.h])
re_F = boxes.edges.CompoundEdge(self, "Fe", [self.h, self.hi-self.h])
elif self.hi < self.h:
# if hi is smaller limit the fingerjoint in the outside walls to hi
ole_f = boxes.edges.CompoundEdge(self, "Ef", [self.h-self.hi, self.hi])
ore_f = boxes.edges.CompoundEdge(self, "fE", [self.hi, self.h-self.hi])
ole_F = boxes.edges.CompoundEdge(self, "EF", [self.h-self.hi, self.hi])
ore_F = boxes.edges.CompoundEdge(self, "FE", [self.hi, self.h-self.hi])
self.ctx.save()
# Horizontal Walls
for y in range(ly + 1):
if y == 0 or y == ly:
# limit finger holes to h on the outside
h = self.h
self.edges["C"].height = min(self.h, self.hi)
self.edges["D"].height = min(self.h, self.hi)
else:
h = self.hi
self.edges["C"].height = self.hi
self.edges["D"].height = self.hi
start = 0
end = 0
while end < lx:
lengths = []
edges = []
while start < lx and not self.hwalls[y][start]:
start += 1
if start == lx:
break
end = start
while end < lx and self.hwalls[y][end]:
if self.hFloor(end, y):
edges.append("f")
else:
edges.append("E")
lengths.append(self.x[end])
if self.hFloor(end, y) == 0 and self.hFloor(end + 1, y) == 0:
edges.append("EDs"[self.vWalls(end + 1, y)])
else:
edges.append("eCs"[self.vWalls(end + 1, y)])
lengths.append(self.thickness)
end += 1
# remove last "slot"
lengths.pop()
edges.pop()
le = le_f if start == 0 and y not in (0, ly) else (ole_f if start > 0 and y in (0, ly) else "f")
re = re_f if end == lx and y not in (0, ly) else (ore_f if end < lx and y in (0, ly) else "f")
self.rectangularWall(sum(lengths), h, [
boxes.edges.CompoundEdge(self, edges, lengths),
re if self.vWalls(end, y) else "e",
"e",
le if self.vWalls(start, y) else "e"],
callback=[lambda: self.wallLabelsCB(start, end, y)],
move="right")
start = end
self.ctx.restore()
self.rectangularWall(10, max(self.h, self.hi), "ffef", move="up only")
self.ctx.save()
# Vertical Walls
for x in range(lx + 1):
if x == 0 or x == lx:
h = self.h
self.edges["C"].height = min(self.h, self.hi)
self.edges["D"].height = min(self.h, self.hi)
else:
h = self.hi
self.edges["C"].height = self.hi
self.edges["D"].height = self.hi
start = 0
end = 0
while end < ly:
lengths = []
edges = []
while start < ly and not self.vwalls[start][x]:
start += 1
if start == ly:
break
end = start
while end < ly and self.vwalls[end][x]:
if self.vFloor(x, end):
edges.append("f")
else:
edges.append("E")
lengths.append(self.y[end])
if self.vFloor(x, end) == 0 and self.vFloor(x, end + 1) == 0:
edges.append("EDS"[self.hWalls(x, end + 1)])
else:
edges.append("eCs"[self.hWalls(x, end + 1)])
lengths.append(self.thickness)
end += 1
# remove last "slot"
lengths.pop()
edges.pop()
upper = [{"f": "e",
"s": "s",
"S": "s", # abuse for E at bottom
"e": "e",
"E": "e",
"C": "e",
"D": "e"}[e] for e in reversed(edges)]
edges = ["e" if e == "s" else ("E" if e == "S" else e) for e in edges]
les = ["e", le_F, le_f] if start == 0 and x not in (0, lx) else (
["e", ole_F, ole_f] if start > 0 and x in (0, lx) else "eFf")
res = ["e", re_F, re_f] if end == ly and x not in (0, lx) else (
["e", ore_F, ore_f] if end < ly and x in (0, lx) else "eFf")
self.rectangularWall(sum(lengths), h, [
boxes.edges.CompoundEdge(self, edges, lengths),
res[self.hWalls(x, end)],
boxes.edges.CompoundEdge(self, upper, list(reversed(lengths))),
les[self.hWalls(x, start)]],
callback=[lambda: self.wallLabelsCB(start, end, x, x=False)],
move="right")
start = end
self.ctx.restore()
self.rectangularWall(10, max(self.h, self.hi), "ffef", move="up only")
def base_plate(self, callback=None, move=None):
lx = len(self.x)
ly = len(self.y)
t = self.thickness
w = self.edges["F"].startWidth()
b = self.burn
t2 = self.thickness / 2.0
tw = sum(self.x) + (lx - 1) * t + 2 * w
th = sum(self.y) + (ly - 1) * t + 2 * w
if self.move(tw, th, move, True):
return
for i, (x, y, a) in enumerate((
(w, w + b, 0),
(tw - w, w + b, 90),
(tw - w, th - w + b, 180),
(w, th - w + b, 270))):
self.cc(callback, i, x, y, a)
# Horizontal lines
posy = w - t
for y in range(ly, -1, -1):
posx = w
for x in range(lx):
if self.hwalls[y][x]:
e = "F"
else:
e = "e"
if self.labels:
self.text(
f"x {x+1}/{y+1}",
posx+self.x[x]/2,
posy + (t if y > 0 else 0),
color=Color.ANNOTATIONS,
align=("center" if y>0 else "center top"),
fontsize=2*self.thickness)
if self.floors[y][x]:
if self.floors[y - 1][x]:
# Inside Wall
if self.hwalls[y][x]:
self.fingerHolesAt(posx, posy + t2, self.x[x], angle=0)
else:
# Top edge
self.edgeAt(e, posx + self.x[x],
posy + w + b, self.x[x],
-180)
if not self.floors[y - 1][x - 1] and not self.floors[y][x - 1]:
self.cornerAt(posx, posy + w + b, w, 180) # top left corner
if not self.floors[y - 1][x + 1] and not self.floors[y][x + 1]:
self.cornerAt(posx + self.x[x] + w + b, posy, w, 90) # top right corner
if not self.floors[y - 1][x - 1] and self.floors[y][x - 1]:
self.edgeAt("e", posx - t, posy + w + b, t, 0) # top edge under wall
elif self.floors[y - 1][x]:
# Bottom Edge
self.edgeAt(e, posx, posy - b + t - w, self.x[x])
if not self.floors[y - 1][x - 1] and not self.floors[y][x - 1]:
self.cornerAt(posx - w -b, posy + t , w, -90) # bottom left corner
if not self.floors[y - 1][x + 1] and not self.floors[y][x + 1]:
self.cornerAt(posx + self.x[x], posy + t - w - b, w, 0) # bottom right corner
if self.floors[y - 1][x - 1] and not self.floors[y][x - 1]:
self.edgeAt("e", posx - t, posy + t - w - b, t) # bottom edge under wall
posx += self.x[x] + self.thickness
posy += self.y[y - 1] + self.thickness
posx = w - t
for x in range(lx + 1):
posy = w
for y in range(ly - 1, -1, -1):
if self.vwalls[y][x]:
e = "F"
else:
e = "e"
if self.labels:
self.text(
f"y {x+1}/{y+1}",
posx+ (t if x < lx else 0),
posy+self.y[y]/2,
color=Color.ANNOTATIONS,
angle=-90,
align=("center" if x < lx else "center top"),
fontsize=2*self.thickness)
if self.floors[y][x - 1]:
if self.floors[y][x]:
# Inside wall
if self.vwalls[y][x]:
self.fingerHolesAt(posx + t2, posy, self.y[y])
else:
# Right edge
self.edgeAt(e, posx + w + b, posy, self.y[y], 90)
if self.floors[y-1][x-1] and not self.floors[y - 1][x]:
self.edgeAt("e", posx + w + b, posy + self.y[y], t, 90) # right edge under wall
elif self.floors[y][x]:
# Left edge
self.edgeAt(e, posx + t - w - b, posy + self.y[y], self.y[y], -90)
if self.floors[y - 1][x] and not self.floors[y - 1][x - 1]:
self.edgeAt("e", posx + t - w - b,
posy + self.y[y] + t, t, -90)
posy += self.y[y] + self.thickness
if x < lx:
posx += self.x[x] + self.thickness
self.move(tw, th, move)
def parse(self, input):
x = []
y = []
hwalls = []
vwalls = []
floors = []
for nr, line in enumerate(input):
if not line or line[0] == "#":
continue
m = re.match(r"( \|)* ,>\s*(\d*\.?\d+)\s*mm\s*", line)
if m:
x.append(float(m.group(2)))
continue
if line[0] == '+':
w = []
for n, c in enumerate(line[:len(x) * 2 + 1]):
if n % 2:
if c == ' ':
w.append(False)
elif c == '-':
w.append(True)
else:
pass
# raise ValueError(line)
else:
if c != '+':
pass
# raise ValueError(line)
hwalls.append(w)
if line[0] in " |":
w = []
f = []
for n, c in enumerate(line[:len(x) * 2 + 1]):
if n % 2:
if c in 'xX':
f.append(False)
elif c == ' ':
f.append(True)
else:
raise ValueError("""Can't parse line %i in layout: expected " ", "x" or "X" for char #%i""" % (nr + 1, n + 1))
else:
if c == ' ':
w.append(False)
elif c == '|':
w.append(True)
else:
raise ValueError("""Can't parse line %i in layout: expected " ", or "|" for char #%i""" % (nr + 1, n + 1))
floors.append(f)
vwalls.append(w)
m = re.match(r"([ |][ xX])+[ |]\s*(\d*\.?\d+)\s*mm\s*", line)
if not m:
raise ValueError("""Can't parse line %i in layout: Can read height of the row""" % (nr + 1))
else:
y.append(float(m.group(2)))
# check sizes
lx = len(x)
ly = len(y)
if lx == 0:
raise ValueError("Need more than one wall in x direction")
if ly == 0:
raise ValueError("Need more than one wall in y direction")
if len(hwalls) != ly + 1:
raise ValueError("Wrong number of horizontal wall lines: %i (%i expected)" % (len(hwalls), ly + 1))
for nr, walls in enumerate(hwalls):
if len(walls) != lx:
raise ValueError("Wrong number of horizontal walls in line %i: %i (%i expected)" % (nr, len(walls), lx))
if len(vwalls) != ly:
raise ValueError("Wrong number of vertical wall lines: %i (%i expected)" % (len(vwalls), ly))
for nr, walls in enumerate(vwalls):
if len(walls) != lx + 1:
raise ValueError("Wrong number of vertical walls in line %i: %i (%i expected)" % (nr, len(walls), lx + 1))
self.x = x
self.y = y
self.hwalls = hwalls
self.vwalls = vwalls
self.floors = floors
# Add row of no floor to the right and bottom
# to avoid special casing the borders
# Will also show up as index -1
for l in floors:
l.append(False)
floors.append([False] * (lx+1))
def render(self) -> None:
self.prepare()
self.walls()
self.base_plate(move="up")
self.lid(sum(self.x) + (len(self.x)-1) * self.thickness,
sum(self.y) + (len(self.y)-1) * self.thickness)