diff --git a/boxes/generators/frontpanel.py b/boxes/generators/frontpanel.py
new file mode 100644
index 0000000..8a80fc8
--- /dev/null
+++ b/boxes/generators/frontpanel.py
@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+# Copyright (C) 2013-2017 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 .
+
+from boxes import *
+import io
+import shlex
+
+def str_to_bool(s):
+ if (s.lower() in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh']):
+ return True
+ else:
+ return False
+
+class FrontPanel(Boxes):
+ """Mounting Holes and cutouts for all your holy needs."""
+
+ description = f"""
+
+
+
+
+This will help you create font (and side and top) panels for your
+boxes that are pre-configured for all the bits and bobs you'd like to
+install
+
+ The layout can create several types of holes including rectangles,
+ circles and mounting holes. The default shows an example layout with all
+ currently supported objects.
+
+####
+`rect x y w h [cr=0] [cx=True] [cy=True]`
+
+ x: x position
+ y: y position
+ w: width
+ h: height
+ cr: optional, Corner radius, default=0
+ cx: optional, Center x. the x position denotes the center of the rectangle.
+ accepts t, T, 1, or other true-like values.
+ cy: optional, Center y. the y position denotes the center of the rectangle.
+
+#### outline
+`rect w h`
+
+ w: width
+ h: height
+
+`outline` has a special meaning: You can create multiple panel outlines with one command.
+This has the effect of making it easy to manage all the holes on all the sides of
+your boxes.
+
+#### circle
+`circle x y r`
+
+ x: x position
+ y: y position
+ r: radius
+
+#### mountinghole
+mountinghole x y d_shaft [d_head=0] [angle=0]
+
+ x: x position
+ y: y position
+ d_shaft: diameter of the shaft part of the mounting hole
+ d_head: optional. diameter of the head
+ angle: optional. angle of the mounting hole
+
+#### text
+`text x y size "some text" [angle=0] [align=bottom|left]`
+
+ x: x position
+ y: y position
+ size: size, in mm
+ text: text to render. This *must* be in quotation marks
+ angle: angle (in degrees)
+ align: string with combinations of (top|middle|bottom) and (left|center|right),
+ separated by '|'. Default is 'bottom|left'
+
+
+
+#### nema
+`nema x y size [screwhole_size=0]`
+
+ x: x position (center of shaft)
+ y: y position (center of shaft)
+ size: nema size. One of [{', '.join([f'{x}' for x in Boxes.nema_sizes])}]
+ screw: screw size, in mm. Optional. Default=0, which means the default size
+
+ """
+
+ ui_group = "Holes"
+
+ def __init__(self) -> None:
+ Boxes.__init__(self)
+ self.argparser.add_argument(
+ "--layout", action="store", type=str,
+ default="""
+outline 100 100
+rect 50 60 80 30 3 True False
+text 50 91 7 "Super Front Panel With Buttons!" 0 bottom|center
+circle 10 45 3.5
+circle 30 45 3.5
+circle 50 45 3.5
+circle 70 45 3.5
+circle 90 45 3.5
+text 10 40 3 "BTN_1" 0 top|center
+text 35 45 3 "BTN_2" 90 top|center
+text 50 50 3 "BTN_3" 180 top|center
+text 65 45 3 "BTN_4" 270 top|center
+text 90 45 3 "5" 0 middle|center
+mountinghole 5 85 3 6 90
+mountinghole 95 85 3 6 90
+
+# Start another panel, 30x50
+outline 30 50
+rect 15 25 15 15 1 True True
+text 15 25 3 "__Fun!" 0 bottom|left
+text 15 25 3 "__Fun!" 45 bottom|left
+text 15 25 3 "__Fun!" 90 bottom|left
+text 15 25 3 "__Fun!" 135 bottom|left
+text 15 25 3 "__Fun!" 180 bottom|left
+text 15 25 3 "__Fun!" 225 bottom|left
+text 15 25 3 "__Fun!" 270 bottom|left
+
+text 3 10 2 "Another panel, for fun" 0 top|left
+
+
+# Let's create another panel with a nema motor on it
+outline 40 40
+nema 20 20 17
+""")
+
+ def applyOffset(self, x, y):
+ return (x+self.offset[0], y+self.offset[1])
+
+ def drawRect(self, x, y, w, h, r=0, center_x="True", center_y="True"):
+ x, y, w, h, r = [float(i) for i in [x, y, w, h, r]]
+ x, y = self.applyOffset(x, y)
+ center_x = str_to_bool(center_x)
+ center_y = str_to_bool(center_y)
+ self.rectangularHole(x, y, w, h, r, center_x, center_y)
+ return
+
+ def drawCircle(self, x, y, r):
+ x, y, r = [float(i) for i in [x, y, r]]
+ x, y = self.applyOffset(x, y)
+ self.hole(x, y, r)
+ return
+
+ def drawMountingHole(self, x, y, d_shaft, d_head=0.0, angle=0):
+ x, y, d_shaft, d_head, angle = [float(i) for i in [x, y, d_shaft, d_head, angle]]
+ x, y = self.applyOffset(x, y)
+ self.mountingHole(x, y, d_shaft, d_head, angle)
+ return
+
+ def drawOutline(self, w, h):
+ w, h = [float(i) for i in [w, h]]
+ if self.outline is not None:
+ self.offset = self.applyOffset(self.outline[0]+10, 0)
+ self.outline = (w, h) # store away for next time
+ x = 0
+ y = 0
+ x, y = self.applyOffset(x, y)
+ border = [(x, y), (x+w, y), (x+w, y+h), (x, y+h), (x, y)]
+ self.showBorderPoly( border )
+ return
+
+ def drawText(self, x, y, size, text, angle=0, align='bottom|left'):
+ x, y, size, angle = [float(i) for i in [x, y, size, angle]]
+ x, y = self.applyOffset(x, y)
+ align = align.replace("|", " ")
+ self.text(text=text, x=x, y=y, fontsize=size, angle=angle, align=align)
+
+ def drawNema(self, x, y, size, screwhole_size=0):
+ x, y, size, screwhole_size = [float(i) for i in [x, y, size, screwhole_size]]
+ if size in self.nema_sizes:
+ x, y = self.applyOffset(x, y)
+ self.NEMA(size, x, y, screwholes=screwhole_size)
+
+ def parse_layout(self, layout):
+ f = io.StringIO(layout)
+ line = 0
+ objects = {
+ 'outline': self.drawOutline,
+ 'rect': self.drawRect,
+ 'circle': self.drawCircle,
+ 'mountinghole': self.drawMountingHole,
+ 'text': self.drawText,
+ 'nema': self.drawNema,
+ }
+
+ for l in f.readlines():
+ line += 1
+ l = re.sub('#.*$', '', l) # remove comments
+ l = l.strip()
+ la = shlex.split(l, comments=True, posix=True)
+ if len(la) > 0 and la[0].lower() in objects:
+ objects[la[0]](*la[1:])
+ return
+
+ def render(self):
+ self.offset = (0.0, 0.0)
+ self.outline = None # No outline yet
+ self.parse_layout(self.layout)
diff --git a/boxes/generators/frontpanel_test.json b/boxes/generators/frontpanel_test.json
new file mode 100644
index 0000000..b40683c
--- /dev/null
+++ b/boxes/generators/frontpanel_test.json
@@ -0,0 +1,4 @@
+outline 60 60
+squre 10 20 25 34
+circle 10 20 40
+
diff --git a/static/samples/FrontPanel-thumb.jpg b/static/samples/FrontPanel-thumb.jpg
new file mode 100644
index 0000000..54b362b
Binary files /dev/null and b/static/samples/FrontPanel-thumb.jpg differ
diff --git a/static/samples/FrontPanel.jpg b/static/samples/FrontPanel.jpg
new file mode 100644
index 0000000..322b33c
Binary files /dev/null and b/static/samples/FrontPanel.jpg differ
diff --git a/static/samples/samples.sha256 b/static/samples/samples.sha256
index d083c57..81355c6 100644
--- a/static/samples/samples.sha256
+++ b/static/samples/samples.sha256
@@ -116,3 +116,4 @@ a7ee83b42685295fd02db666841984c55af2f9632540c651f5dea42088a3c170 ../static/samp
ab47e02fb9736d20b457964aa640f50852ac97bb2857cea753fa4c3067a60d41 ../static/samples/Spool.jpg
cfc0782f3a952dd0c6ceb0fb7998050f3bdea135d791fa32b731808371514e63 ../static/samples/GearBox.jpg
75733dfdfd601ace1521bddfea28546ca34d8281acbeb6ec44f15b2b942cb944 ../static/samples/Arcade.jpg
+a21471512fd73c15e1d8a11aa3bd4ef807791895855c2f1d30a00bd207d79919 ../static/samples/FrontPanel.jpg