boxespy/boxes/generators/traylayout.py

459 lines
17 KiB
Python

#!/usr/bin/env python3
# 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 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) -> None:
# Use empty open and close methods to avoid initializing the whole drawing infrastructure.
pass
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)
with open(self.output, 'w') as f:
f.write(str(self))
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")
if self.UI == "web":
self.argparser.add_argument(
"--layout", action="store", type=str, default="")
else:
self.argparser.add_argument(
"--input", action="store", type=argparse.FileType('r'),
default="traylayout.txt",
help="layout file")
self.layout = None
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)
def prepare(self):
if self.layout:
self.parse(self.layout.split('\n'))
else:
self.parse(self.input)
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 walls(self, move=None):
lx = len(self.x)
ly = len(self.y)
# t = self.thickness
# b = self.burn
# t2 = self.thickness / 2.0
self.ctx.save()
# Horizontal Walls
for y in range(ly + 1):
if y == 0 or y == ly:
h = self.h
else:
h = 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()
self.rectangularWall(sum(lengths), h, [
boxes.edges.CompoundEdge(self, edges, lengths),
"f" if self.vWalls(end, y) else "e",
"e",
"f" if self.vWalls(start, y) else "e"],
move="right")
start = end
self.ctx.restore()
self.rectangularWall(10, h, "ffef", move="up only")
self.ctx.save()
# Vertical Walls
for x in range(lx + 1):
if x == 0 or x == lx:
h = self.h
else:
h = 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",
"e": "e",
"E": "e",
"C": "e",
"D": "e"}[e] for e in reversed(edges)]
edges = ["e" if e == "s" else e for e in edges]
self.rectangularWall(sum(lengths), h, [
boxes.edges.CompoundEdge(self, edges, lengths),
"eFf"[self.hWalls(x, end)],
boxes.edges.CompoundEdge(self, upper, list(reversed(lengths))),
"eFf"[self.hWalls(x, start)]],
move="right")
start = end
self.ctx.restore()
self.rectangularWall(10, h, "ffef", move="up only")
def base_plate(self, callback=None, move=None):
lx = len(self.x)
ly = len(self.y)
t = self.thickness
b = self.burn
t2 = self.thickness / 2.0
tw, th = sum(self.x) + (lx + 1) * t, sum(self.y) + (ly + 1) * t
if self.move(tw, th, move, True):
return
for i, (x, y, a) in enumerate((
(t, t + b, 0),
(tw - t, t + b, 90),
(tw - t, th - t + b, 180),
(t, th - t + b, 270))):
self.cc(callback, i, x, y, a)
# Horizontal lines
posy = 0
for y in range(ly, -1, -1):
posx = self.thickness
for x in range(lx):
if self.hwalls[y][x]:
e = "F"
else:
e = "e"
if y < ly and self.floors[y][x]:
if y > 0 and 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 + t + b, self.x[x],
-180)
if x == 0 or y == 0 or not self.floors[y - 1][x - 1]:
self.edgeAt("e", posx, posy + t + b, t, -180)
if x == lx - 1 or y == 0 or not self.floors[y - 1][x + 1]:
self.edgeAt("e", posx + self.x[x] + t, posy + t + b, t, -180)
elif y > 0 and self.floors[y - 1][x]:
# Bottom Edge
self.edgeAt(e, posx, posy - b, self.x[x])
if x == 0 or y == ly or not self.floors[y][x - 1]:
self.edgeAt("e", posx - t, posy - b, t)
if x == lx - 1 or y == ly or not self.floors[y][x + 1]:
self.edgeAt("e", posx + self.x[x], posy - b, t)
posx += self.x[x] + self.thickness
posy += self.y[y - 1] + self.thickness
posx = 0
for x in range(lx + 1):
posy = self.thickness
for y in range(ly - 1, -1, -1):
if self.vwalls[y][x]:
e = "F"
else:
e = "e"
if x > 0 and self.floors[y][x - 1]:
if x < lx and 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 + t + b, posy, self.y[y], 90)
if x == lx or y == 0 or not self.floors[y - 1][x]:
self.edgeAt("e", posx + t + b, posy + self.y[y], t, 90)
if x == lx or y == ly - 1 or not self.floors[y + 1][x]:
self.edgeAt("e", posx + t + b, posy - t, t, 90)
elif x < lx and self.floors[y][x]:
# Left edge
self.edgeAt(e, posx - b, posy + self.y[y], self.y[y], -90)
if x == 0 or y == 0 or not self.floors[y - 1][x - 1]:
self.edgeAt("e", posx - b, posy + self.y[y] + t, t, -90)
if x == 0 or y == ly - 1 or not self.floors[y + 1][x - 1]:
self.edgeAt("e", posx - b, posy, 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
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)