diff --git a/.gitignore b/.gitignore
index 70caafc6..51b88216 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*~
*.plt
*.svg
+!examples/*.svg
*.dxf
*.pyc
*.pwj
diff --git a/boxes/generators/wallstackablebin.py b/boxes/generators/wallstackablebin.py
new file mode 100644
index 00000000..56758cff
--- /dev/null
+++ b/boxes/generators/wallstackablebin.py
@@ -0,0 +1,169 @@
+# Copyright (C) 2024 Alex Roberts
+#
+# 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 *
+from boxes.walledges import _WallMountedBox
+
+class StackableBinEdge(edges.BaseEdge):
+ # Character used to identify this edge type in wall specifications
+ char = "j"
+
+ def __call__(self, length, **kw):
+ # ASCII art showing edge profile geometry:
+ # _
+ # | /
+ # | / b <- Angle between vertical and hyp2
+ # | /
+ # | hyp2 / <- Upper hypotenuse length
+ # | /
+ # | /
+ # _ | /
+ # | | |
+ # | | |
+ # l h | <- Total height
+ # | | |
+ # _ _ | | ---- w ---- <- Width of slope projection
+ # | | \
+ # | | \
+ # f | hyp1 \ <- Lower hypotenuse length
+ # | | \
+ # | | \ a <- Base angle from settings
+ # _ _ \
+
+ # Get configuration from settings
+ a = self.settings.angle # Base angle for slope
+ f = self.settings.front # Fraction of height for front slope
+ l = self.settings.label_height # Fraction of height for label
+ label = self.settings.label # Whether to include label area
+ h = self.settings.h # Total height of bin
+
+ # If label is disabled, set label height to 0
+ if not label:
+ l = 0
+
+ # Calculate key dimensions:
+ w = (h * f) * math.tan(math.radians(a)) # Width of slope projection
+ hyp1 = (h * f) / math.cos(math.radians(a)) # Length of lower slope
+ hyp2 = math.sqrt(w**2 + ((1-(l+f))*h)**2) # Length of upper slope
+ b = math.degrees(math.atan(w/((1-l-f)*h))) # Angle between vertical and hyp2
+
+ # Draw the edge profile sequence:
+ self.corner(-b) # Rotate to start upper diagonal
+ self.edges["e"](hyp2) # Draw upper diagonal section
+ self.corner(b) # Return to vertical
+ self.edges["f"](l*h) # Draw label section if enabled
+ self.corner(a) # Rotate to lower diagonal
+ self.edges["f"](hyp1) # Draw lower diagonal section
+ self.corner(-a) # Return to horizontal
+
+ def margin(self) -> float:
+ # Calculate the margin required for this edge type
+ # Same calculation as width of slope projection
+ # Add material thickness if a label area is included
+ t = self.settings.thickness if self.settings.label else 0
+ return (self.settings.h * self.settings.front) * math.tan(math.radians(self.settings.angle)) + t
+
+class WallStackableBin(_WallMountedBox):
+ """A wall-mounted bin that can stack or hang from a wall."""
+ description = '''
+####Features:
+- Configurable dimensions (width, height, depth)
+- Adjustable front slope angle for easy access
+- Optional label area on front face
+- Stackable design with interlocking geometry
+- Multiple wall mounting options
+
+####Stacking Compatibility:
+The stacking mechanism requires careful consideration of wall mounting types.
+Bins using "plain" wall mounts can stack on bins with other mount types.
+Note: Using the "outside" dimension option is generally not compatible with stacking.
+
+####Assembly Notes:
+1. The generator produces three pieces with angled finger joints.
+Bottom panel, sloped front panel and label panel (if enabled).
+2. Joint lengths vary to accommodate the slope angles
+3. Orient pieces as shown in the generated layout to assemble correctly.
+'''
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.addSettingsArgs(edges.StackableSettings, bottom_stabilizers=2.4)
+
+ self.buildArgParser("outside")
+ self.buildArgParser(x=90, h=130, y=150)
+
+ self.argparser.add_argument(
+ "--front", action="store", type=float, default=0.3,
+ help="fraction of bin height covered with slope")
+ self.argparser.add_argument(
+ "--angle", action="store", type=float, default=30,
+ help="angle of the bottom slope")
+
+ self.argparser.add_argument(
+ "--label", action="store", type=boolarg, default=True,
+ help="include a label area on the front")
+ self.argparser.add_argument(
+ "--label_height", action="store", type=float, default=0.2,
+ help="fraction of bin height covered with label (if label is enabled)")
+
+ def render(self):
+ # Generate standard wall mounting edges (A-D and vertical separator |)
+ self.generateWallEdges()
+
+ # Add custom stackable bin edge for the front profile
+ self.addPart(StackableBinEdge(self, self))
+
+ # Configure angled finger joints for the sloped sections
+ # First set: For bottom-to-slope connection
+ angledsettings = copy.deepcopy(self.edges["f"].settings)
+ angledsettings.setValues(self.thickness, True, angle=90-self.angle)
+ angledsettings.edgeObjects(self, chars="gG")
+
+ # Second set: For slope-to-label connection
+ angledsettings = copy.deepcopy(self.edges["f"].settings)
+ angledsettings.setValues(self.thickness, True, angle=self.angle)
+ angledsettings.edgeObjects(self, chars="kK")
+
+ # Adjust dimensions if outside measurements specified
+ if self.outside:
+ self.x = self.adjustSize(self.x)
+ self.h = self.adjustSize(self.h, "s", "S")
+ self.y = self.adjustSize(self.y, "h", "b")
+
+ with self.saved_context():
+ # Bottom panel with finger joints
+ self.rectangularWall(self.x, self.y, "ffGf", label="bottom", move="up")
+
+ if self.label:
+ # Sloped front with label area
+ self.rectangularWall(self.x, self.h*self.front/math.cos(math.radians(self.angle)),
+ "gFkF", label="slope", move="up")
+ # Label panel
+ self.rectangularWall(self.x, self.h*self.label_height,
+ "KFeF", label="label", move="up")
+ else:
+ # Sloped front without label
+ self.rectangularWall(self.x, self.h*self.front/math.cos(math.radians(self.angle)),
+ "gFeF", label="slope", move="up")
+
+ # Back panel with wall mount edges
+ self.rectangularWall(self.x, self.h, "hCec", label="back", move="up")
+
+ # Non drawn spacer to move wall pieces to the right
+ self.rectangularWall(self.x, 3, "DDDD", label="movement", move="right only")
+
+ # Side panels with wall mount and stackable edges
+ self.rectangularWall(self.y, self.h, "sBSj", label="left side", move="up")
+ self.rectangularWall(self.y, self.h, "sBSj", label="right side", move="mirror up")
diff --git a/examples/WallStackableBin.svg b/examples/WallStackableBin.svg
new file mode 100644
index 00000000..05cb316e
--- /dev/null
+++ b/examples/WallStackableBin.svg
@@ -0,0 +1,123 @@
+
+
\ No newline at end of file
diff --git a/static/samples/WallStackableBin-thumb.jpg b/static/samples/WallStackableBin-thumb.jpg
new file mode 100644
index 00000000..a10d1808
Binary files /dev/null and b/static/samples/WallStackableBin-thumb.jpg differ
diff --git a/static/samples/WallStackableBin.jpg b/static/samples/WallStackableBin.jpg
new file mode 100644
index 00000000..e25d51e6
Binary files /dev/null and b/static/samples/WallStackableBin.jpg differ
diff --git a/static/samples/samples.sha256 b/static/samples/samples.sha256
index 0e60f0af..9e229057 100644
--- a/static/samples/samples.sha256
+++ b/static/samples/samples.sha256
@@ -189,3 +189,4 @@ d7f7fd6c1b5a51c4fdfc21a03d55597f04d78383fef2138bc6ade4ee95676bc9 ../static/samp
fec237bd76c18f1cad18a888f57299e5ff5033ab8032e7a26ea0b1259a42d150 ../static/samples/CompartmentBox-lid.jpg
5c2127a79948504f629ed792de539022411e428eb46dc8ad9c5286fb397cd603 ../static/samples/CompartmentBox.jpg
ca53e3c8b9ba8d46ca2d6fdff24865514dde27380b409fef82f0b87ac0bada2d ../static/samples/HobbyCase.jpg
+478769d7f422d0e47d50c7c1476b72a269f9acd03042d2d16dc0c00fa80941f6 ../static/samples/WallStackableBin.jpg