# Copyright (C) 2013-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 argparse
import inspect
import math
import re
from abc import ABC, abstractmethod
from typing import Any
from boxes import gears
[docs]
def argparseSections(s: str) -> list[float]:
"""
Parse sections parameter
:param s: string to parse
"""
result: list[float] = []
parse = re.split(r"\s|:", s)
try:
for part in parse:
m = re.match(r"^(\d+(\.\d+)?)/(\d+)$", part)
if m:
n = int(m.group(3))
result.extend([float(m.group(1)) / n] * n)
continue
m = re.match(r"^(\d+(\.\d+)?)\*(\d+)$", part)
if m:
n = int(m.group(3))
result.extend([float(m.group(1))] * n)
continue
result.append(float(part))
except ValueError:
raise argparse.ArgumentTypeError("Don't understand sections string")
if not result:
result.append(0.0)
return result
[docs]
def getDescriptions() -> dict:
d = {edge.char: edge.description for edge in globals().values()
if inspect.isclass(edge) and issubclass(edge, BaseEdge)
and edge.char}
d['j'] = d['i'] + " (other end)"
d['J'] = d['I'] + " (other end)"
d['k'] = d['i'] + " (both ends)"
d['K'] = d['I'] + " (both ends)"
d['O'] = d['o'] + ' (other end)'
d['P'] = d['p'] + ' (other end)'
d['U'] = d['u'] + ' top side'
d['v'] = d['u'] + ' for 90° lid'
d['V'] = d['u'] + ' 90° lid'
return d
[docs]
class BoltPolicy(ABC):
"""Abstract class
Distributes (bed) bolts on a number of segments
(fingers of a finger joint)
"""
[docs]
def drawbolt(self, pos) -> bool:
"""Add a bolt to this segment?
:param pos: number of the finger
"""
return False
[docs]
def numFingers(self, numFingers: int) -> int:
"""Return next smaller, possible number of fingers
:param numFingers: number of fingers to aim for
"""
return numFingers
def _even(self, numFingers: int) -> int:
"""
Return same or next smaller even number
:param numFingers:
"""
return (numFingers // 2) * 2
def _odd(self, numFingers: int) -> int:
"""
Return same or next smaller odd number
:param numFingers:
"""
if numFingers % 2:
return numFingers
return numFingers - 1
[docs]
class Bolts(BoltPolicy):
"""Distribute a fixed number of bolts evenly"""
def __init__(self, bolts: int = 1) -> None:
self.bolts = bolts
[docs]
def numFingers(self, numFingers: int) -> int:
if self.bolts % 2:
self.fingers = self._even(numFingers)
else:
self.fingers = numFingers
return self.fingers
[docs]
def drawBolt(self, pos):
"""
Return if this finger needs a bolt
:param pos: number of this finger
"""
if pos > self.fingers // 2:
pos = self.fingers - pos
if pos == 0:
return False
if pos == self.fingers // 2 and not (self.bolts % 2):
return False
return (math.floor((float(pos) * (self.bolts + 1) / self.fingers) - 0.01) !=
math.floor((float(pos + 1) * (self.bolts + 1) / self.fingers) - 0.01))
#############################################################################
### Settings
#############################################################################
[docs]
class Settings:
"""Generic Settings class
Used by different other classes to store measurements and details.
Supports absolute values and settings that grow with the thickness
of the material used.
Overload the absolute_params and relative_params class attributes with
the supported keys and default values. The values are available via
attribute access.
Store values that are not supposed to be changed by the users in class or
instance properties. This way API users can set them as needed while still
be shared between all (Edge) instances using this settings object.
"""
absolute_params: dict[str, Any] = {} # TODO find better typing.
relative_params: dict[str, Any] = {} # TODO find better typing.
[docs]
@classmethod
def parserArguments(cls, parser, prefix=None, **defaults):
prefix = prefix or cls.__name__[:-len("Settings")]
lines = cls.__doc__.split("\n")
# Parse doc string
descriptions = {}
r = re.compile(r"^ +\* +(\S+) +: .* : +(.*)")
for l in lines:
m = r.search(l)
if m:
descriptions[m.group(1)] = m.group(2)
group = parser.add_argument_group(lines[0] or lines[1])
group.prefix = prefix
for name, default in (sorted(cls.absolute_params.items()) +
sorted(cls.relative_params.items())):
# Handle choices
choices = None
if isinstance(default, tuple):
choices = default
t = type(default[0])
for val in default:
if (type(val) is not t or
type(val) not in (bool, int, float, str)):
raise ValueError("Type not supported: %r", val)
default = default[0]
# Overwrite default
if name in defaults:
default = type(default)(defaults[name])
if type(default) not in (bool, int, float, str):
raise ValueError("Type not supported: %r", default)
if type(default) is bool:
from boxes import BoolArg
t = BoolArg()
else:
t = type(default)
group.add_argument(f"--{prefix}_{name}",
type=t,
action="store", default=default,
choices=choices,
help=descriptions.get(name))
def __init__(self, thickness, relative: bool = True, **kw) -> None:
self.values = {}
for name, value in self.absolute_params.items():
if isinstance(value, tuple):
value = value[0]
if type(value) not in (bool, int, float, str):
raise ValueError("Type not supported: %r", value)
self.values[name] = value
self.thickness = thickness
factor = 1.0
if relative:
factor = thickness
for name, value in self.relative_params.items():
self.values[name] = value * factor
self.setValues(thickness, relative, **kw)
[docs]
def edgeObjects(self, boxes, chars: str = "", add: bool = True):
"""
Generate Edge objects using this kind of settings
:param boxes: Boxes object
:param chars: sequence of chars to be used by Edge objects
:param add: add the resulting Edge objects to the Boxes object's edges
"""
edges: list[Any] = []
return self._edgeObjects(edges, boxes, chars, add)
def _edgeObjects(self, edges, boxes, chars: str, add: bool):
for i, edge in enumerate(edges):
try:
char = chars[i]
edge.char = char
except IndexError:
pass
except TypeError:
pass
if add:
boxes.addParts(edges)
return edges
[docs]
def setValues(self, thickness, relative: bool = True, **kw):
"""
Set values
:param thickness: thickness of the material used
:param relative: Do scale by thickness (Default value = True)
:param kw: parameters to set
"""
factor = 1.0
if relative:
factor = thickness
for name, value in kw.items():
if name in self.absolute_params:
self.values[name] = value
elif name in self.relative_params:
self.values[name] = value * factor
elif hasattr(self, name):
setattr(self, name, value)
else:
raise ValueError(f"Unknown parameter for {self.__class__.__name__}: {name}")
self.checkValues()
[docs]
def checkValues(self) -> None:
"""
Check if all values are in the right range. Raise ValueError if needed.
"""
pass
def __getattr__(self, name):
if "values" in self.__dict__ and name in self.values:
return self.values[name]
raise AttributeError
#############################################################################
### Edges
#############################################################################
[docs]
class BaseEdge(ABC):
"""Abstract base class for all Edges"""
char: str | None = None
description: str = "Abstract Edge Class"
def __init__(self, boxes, settings) -> None:
self.boxes = boxes
self.ctx = boxes.ctx
self.settings = settings
def __getattr__(self, name):
"""Hack for using unalter code form Boxes class"""
return getattr(self.boxes, name)
[docs]
@abstractmethod
def __call__(self, length, **kw):
pass
[docs]
def startwidth(self) -> float:
"""Amount of space the beginning of the edge is set below the inner space of the part """
return 0.0
[docs]
def endwidth(self) -> float:
return self.startwidth()
[docs]
def margin(self) -> float:
"""Space needed right of the starting point"""
return 0.0
[docs]
def spacing(self) -> float:
"""Space the edge needs outside of the inner space of the part"""
return self.startwidth() + self.margin()
[docs]
def startAngle(self) -> float:
"""Not yet supported"""
return 0.0
[docs]
def endAngle(self) -> float:
"""Not yet supported"""
return 0.0
[docs]
class Edge(BaseEdge):
"""Straight edge"""
char = 'e'
description = "Straight Edge"
positive = False
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
"""Draw edge of length mm"""
if bedBolts:
# distribute the bolts equidistantly
interval_length = length / bedBolts.bolts
if self.positive:
d = (bedBoltSettings or self.bedBoltSettings)[0]
for i in range(bedBolts.bolts):
self.hole(0.5 * interval_length,
0.5 * self.thickness, 0.5 * d)
self.edge(interval_length, tabs=
(i == 0 or i == bedBolts.bolts - 1))
else:
for i in range(bedBolts.bolts):
self.bedBoltHole(interval_length, bedBoltSettings, tabs=
(i == 0 or i == bedBolts.bolts - 1))
else:
self.edge(length, tabs=2)
[docs]
class OutSetEdge(Edge):
"""Straight edge out set by one thickness"""
char = 'E'
description = "Straight Edge (outset by thickness)"
positive = True
[docs]
def startwidth(self) -> float:
return self.settings if self.settings is not None else self.boxes.thickness
[docs]
class NoopEdge(BaseEdge):
"""
Edge which does nothing, not even turn or move.
"""
def __init__(self, boxes, margin=0) -> None:
super().__init__(boxes, None)
self._margin = margin
def __call__(self, _, **kw):
# cancel turn
self.corner(-90)
[docs]
def margin(self) -> float:
return self._margin
#############################################################################
#### MountingEdge
#############################################################################
[docs]
class MountingSettings(Settings):
"""Settings for Mounting Edge
Values:
* absolute_params
* style : "straight edge, within" : edge style
* side : "back" : side of box (not all valid configurations make sense...)
* num : 2 : number of mounting holes (integer)
* margin : 0.125 : minimum space left and right without holes (fraction of the edge length)
* d_shaft : 3.0 : shaft diameter of mounting screw (in mm)
* d_head : 6.5 : head diameter of mounting screw (in mm)
"""
PARAM_IN = "straight edge, within"
PARAM_EXT = "straight edge, extended"
PARAM_TAB = "mounting tab"
PARAM_LEFT = "left"
PARAM_BACK = "back"
PARAM_RIGHT = "right"
PARAM_FRONT = "front"
absolute_params = {
"style": (PARAM_IN, PARAM_EXT, PARAM_TAB),
"side": (PARAM_BACK, PARAM_LEFT, PARAM_RIGHT, PARAM_FRONT),
"num": 2,
"margin": 0.125,
"d_shaft": 3.0,
"d_head": 6.5
}
[docs]
def edgeObjects(self, boxes, chars: str = "G", add: bool = True):
edges = [MountingEdge(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class MountingEdge(BaseEdge):
description = """Edge with pear shaped mounting holes""" # for slide-on mounting using flat-head screws"""
char = 'G'
[docs]
def margin(self) -> float:
if self.settings.style == MountingSettings.PARAM_TAB:
return 2.75 * self.boxes.thickness + self.settings.d_head
return 0.0
[docs]
def startwidth(self) -> float:
if self.settings.style == MountingSettings.PARAM_EXT:
return 2.5 * self.boxes.thickness + self.settings.d_head
return 0.0
def __call__(self, length, **kw):
if length == 0.0:
return
def check_bounds(val, mn, mx, name):
if not mn <= val <= mx:
raise ValueError(f"MountingEdge: {name} needs to be in [{mn}, {mx}] but is {val}")
style = self.settings.style
margin = self.settings.margin
num = self.settings.num
ds = self.settings.d_shaft
dh = self.settings.d_head
if dh > 0:
width = 3 * self.thickness + dh
else:
width = ds
if num != int(num):
raise ValueError(f"MountingEdge: num needs to be an integer number")
check_bounds(margin, 0, 0.5, "margin")
if not dh == 0:
if not dh > ds:
raise ValueError(f"MountingEdge: d_shaft needs to be in 0 or > {ds}, but is {dh}")
# Check how many holes fit
count = max(1, int(num))
if count > 1:
margin_ = length * margin
gap = (length - 2 * margin_ - width * count) / (count - 1)
if gap < width:
count = int(((length - 2 * margin + width) / (2 * width)) - 0.5)
if count < 1:
self.edge(length)
return
if count < 2:
margin_ = (length - width) / 2
gap = 0
else:
gap = (length - 2 * margin_ - width * count) / (count - 1)
else:
margin_ = (length - width) / 2
gap = 0
if style == MountingSettings.PARAM_TAB:
# The edge until the first groove
self.edge(margin_, tabs=1)
for i in range(count):
if i > 0:
self.edge(gap)
self.corner(-90, self.thickness / 2)
self.edge(dh + 1.5 * ds - self.thickness / 4 - dh / 2)
self.corner(90, self.thickness + dh / 2)
self.corner(-90)
self.corner(90)
self.mountingHole(0, self.thickness * 1.25 + ds / 2, ds, dh, -90)
self.corner(90, self.thickness + dh / 2)
self.edge(dh + 1.5 * ds - self.thickness / 4 - dh / 2)
self.corner(-90, self.thickness / 2)
# The edge until the end
self.edge(margin_, tabs=1)
else:
x = margin_
for i in range(count):
x += width / 2
self.mountingHole(x, ds / 2 + self.thickness * 1.5, ds, dh, -90)
x += width / 2
x += gap
self.edge(length)
#############################################################################
#### GroovedEdge
#############################################################################
[docs]
class GroovedSettings(Settings):
"""Settings for Grooved Edge
Values:
* absolute_params
* style : "arc" : the style of grooves
* tri_angle : 30 : the angle of triangular cuts
* arc_angle : 120 : the angle of arc cuts
* width : 0.2 : the width of each groove (fraction of the edge length)
* gap : 0.1 : the gap between grooves (fraction of the edge length)
* margin : 0.3 : minimum space left and right without grooves (fraction of the edge length)
* inverse : False : invert the groove directions
* interleave : False : alternate the direction of grooves
"""
PARAM_ARC = "arc"
PARAM_FLAT = "flat"
PARAM_SOFTARC = "softarc"
PARAM_TRIANGLE = "triangle"
absolute_params = {
"style": (PARAM_ARC, PARAM_FLAT, PARAM_TRIANGLE, PARAM_SOFTARC),
"tri_angle": 30,
"arc_angle": 120,
"width": 0.2,
"gap": 0.1,
"margin": 0.3,
"inverse": False,
"interleave": False,
}
[docs]
def edgeObjects(self, boxes, chars: str = "zZ", add: bool = True):
edges = [GroovedEdge(boxes, self),
GroovedEdgeCounterPart(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class GroovedEdgeBase(BaseEdge):
[docs]
def is_inverse(self) -> bool:
return self.settings.inverse != self.inverse
[docs]
def groove_arc(self, width, angle: float = 90.0, inv: float = -1.0) -> None:
side_length = width / math.sin(math.radians(angle)) / 2
self.corner(inv * -angle)
self.corner(inv * angle, side_length)
self.corner(inv * angle, side_length)
self.corner(inv * -angle)
[docs]
def groove_soft_arc(self, width, angle: float = 60.0, inv: float = -1.0) -> None:
side_length = width / math.sin(math.radians(angle)) / 4
self.corner(inv * -angle, side_length)
self.corner(inv * angle, side_length)
self.corner(inv * angle, side_length)
self.corner(inv * -angle, side_length)
[docs]
def groove_triangle(self, width, angle: float = 45.0, inv: float = -1.0) -> None:
side_length = width / math.cos(math.radians(angle)) / 2
self.corner(inv * -angle)
self.edge(side_length)
self.corner(inv * 2 * angle)
self.edge(side_length)
self.corner(inv * -angle)
def __call__(self, length, **kw):
if length == 0.0:
return
def check_bounds(val, mn, mx, name):
if not mn <= val <= mx:
raise ValueError(f"{name} needs to be in [{mn}, {mx}] but is {val}")
style = self.settings.style
width = self.settings.width
margin = self.settings.margin
gap = self.settings.gap
interleave = self.settings.interleave
check_bounds(width, 0, 1, "width")
check_bounds(margin, 0, 0.5, "margin")
check_bounds(gap, 0, 1, "gap")
# Check how many grooves fit
count = max(0, int((1 - 2 * margin + gap) / (width + gap)))
inside_width = max(0, count * (width + gap) - gap)
margin = (1 - inside_width) / 2
# Convert to actual length
margin = length * margin
gap = length * gap
width = length * width
# Determine the initial inversion
inv = 1 if self.is_inverse() else -1
if interleave and self.inverse and count % 2 == 0:
inv = -inv
# The edge until the first groove
self.edge(margin, tabs=1)
# Grooves
for i in range(count):
if i > 0:
self.edge(gap)
if interleave:
inv = -inv
if style == GroovedSettings.PARAM_FLAT:
self.edge(width)
elif style == GroovedSettings.PARAM_ARC:
angle = self.settings.arc_angle / 2
self.groove_arc(width, angle, inv)
elif style == GroovedSettings.PARAM_SOFTARC:
angle = self.settings.arc_angle / 2
self.groove_soft_arc(width, angle, inv)
elif style == GroovedSettings.PARAM_TRIANGLE:
angle = self.settings.tri_angle
self.groove_triangle(width, angle, inv)
else:
raise ValueError("Unknown GroovedEdge style: %s)" % style)
# The final edge
self.edge(margin, tabs=1)
[docs]
class GroovedEdge(GroovedEdgeBase):
description = """Edge with grooves"""
char = 'z'
inverse = False
[docs]
class GroovedEdgeCounterPart(GroovedEdgeBase):
description = """Edge with grooves (opposing side)"""
char = 'Z'
inverse = True
#############################################################################
#### Gripping Edge
#############################################################################
[docs]
class GripSettings(Settings):
"""Settings for GrippingEdge
Values:
* absolute_params
* style : "wave" : "wave" or "bumps"
* outset : True : extend outward the straight edge
* relative (in multiples of thickness)
* depth : 0.3 : depth of the grooves
"""
absolute_params = {
"style": ("wave", "bumps"),
"outset": True,
}
relative_params = {
"depth": 0.3,
}
[docs]
def edgeObjects(self, boxes, chars: str = "g", add: bool = True):
edges = [GrippingEdge(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class GrippingEdge(BaseEdge):
description = """Corrugated edge useful as an gipping area"""
char = 'g'
[docs]
def wave(self, length) -> None:
depth = self.settings.depth
grooves = int(length // (depth * 2.0)) + 1
depth = length / grooves / 4.0
o = 1 if self.settings.outset else -1
for groove in range(grooves):
self.corner(o * -90, depth)
self.corner(o * 180, depth)
self.corner(o * -90, depth)
[docs]
def bumps(self, length) -> None:
depth = self.settings.depth
grooves = int(length // (depth * 2.0)) + 1
depth = length / grooves / 2.0
o = 1 if self.settings.outset else -1
if self.settings.outset:
self.corner(-90)
else:
self.corner(90)
self.edge(depth)
self.corner(-180)
for groove in range(grooves):
self.corner(180, depth)
self.corner(-180, 0)
if self.settings.outset:
self.corner(90)
else:
self.edge(depth)
self.corner(90)
[docs]
def margin(self) -> float:
if self.settings.outset:
return self.settings.depth
return 0.0
def __call__(self, length, **kw):
if length == 0.0:
return
getattr(self, self.settings.style)(length)
[docs]
class CompoundEdge(BaseEdge):
"""Edge composed of multiple different Edges"""
description = "Compound Edge"
def __init__(self, boxes, types, lengths) -> None:
super().__init__(boxes, None)
self.types = [self.edges.get(edge, edge) for edge in types]
self.lengths = lengths
self.length = sum(lengths)
[docs]
def startwidth(self) -> float:
return self.types[0].startwidth()
[docs]
def endwidth(self) -> float:
return self.types[-1].endwidth()
[docs]
def margin(self) -> float:
return max(e.margin() + e.startwidth() for e in self.types) - self.types[0].startwidth()
def __call__(self, length, **kw):
if length and abs(length - self.length) > 1E-5:
raise ValueError("Wrong length for CompoundEdge")
lastwidth = self.types[0].startwidth()
for e, l in zip(self.types, self.lengths):
self.step(e.startwidth() - lastwidth)
e(l)
lastwidth = e.endwidth()
#############################################################################
#### Slots
#############################################################################
[docs]
class Slot(BaseEdge):
"""Edge with a slot to slide another piece through """
description = "Slot"
def __init__(self, boxes, depth) -> None:
super().__init__(boxes, None)
self.depth = depth
def __call__(self, length, **kw):
if self.depth:
self.boxes.corner(90)
self.boxes.edge(self.depth)
self.boxes.corner(-90)
self.boxes.edge(length)
self.boxes.corner(-90)
self.boxes.edge(self.depth)
self.boxes.corner(90)
else:
self.boxes.edge(self.length)
[docs]
class SlottedEdge(BaseEdge):
"""Edge with multiple slots"""
description = "Straight Edge with slots"
def __init__(self, boxes, sections, edge: str = "e", slots: int = 0) -> None:
super().__init__(boxes, Settings(boxes.thickness))
self.edge = self.edges.get(edge, edge)
self.sections = sections
self.slots = slots
[docs]
def startwidth(self) -> float:
return self.edge.startwidth()
[docs]
def endwidth(self) -> float:
return self.edge.endwidth()
[docs]
def margin(self) -> float:
return self.edge.margin()
def __call__(self, length, **kw):
for l in self.sections[:-1]:
self.edge(l)
if self.slots:
Slot(self.boxes, self.slots)(self.settings.thickness)
else:
self.boxes.edge(self.settings.thickness)
self.edge(self.sections[-1])
#############################################################################
#### Finger Joints
#############################################################################
[docs]
class FingerJointSettings(Settings):
"""Settings for Finger Joints
Values:
* absolute
* style : "rectangular" : style of the fingers
* surroundingspaces : 2.0 : space at the start and end in multiple of normal spaces
* relative (in multiples of thickness)
* space : 2.0 : space between fingers (multiples of thickness)
* finger : 2.0 : width of the fingers (multiples of thickness)
* width : 1.0 : width of finger holes (multiples of thickness)
* edge_width : 1.0 : space below holes of FingerHoleEdge (multiples of thickness)
* play : 0.0 : extra space to allow finger move in and out (multiples of thickness)
* extra_length : 0.0 : extra material to grind away burn marks (multiples of thickness)
* bottom_lip : 0.0 : height of the bottom lips sticking out (multiples of thickness) FingerHoleEdge only!
"""
absolute_params = {
"style": ("rectangular", "springs", "barbs", "snap"),
"surroundingspaces": 2.0,
}
relative_params = {
"space": 2.0,
"finger": 2.0,
"width": 1.0,
"edge_width": 1.0,
"play": 0.0,
"extra_length": 0.0,
"bottom_lip": 0.0,
}
angle = 90 # Angle of the walls meeting
[docs]
def checkValues(self) -> None:
if abs(self.space + self.finger) < 0.1:
raise ValueError("FingerJointSettings: space + finger must not be close to zero")
[docs]
def edgeObjects(self, boxes, chars: str = "fFh", add: bool = True):
edges = [FingerJointEdge(boxes, self),
FingerJointEdgeCounterPart(boxes, self),
FingerHoleEdge(boxes, self),
]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class FingerJointBase(ABC):
"""Abstract base class for finger joint."""
[docs]
def calcFingers(self, length: float, bedBolts) -> tuple[int, float]:
space, finger = self.settings.space, self.settings.finger # type: ignore
fingers = int((length - (self.settings.surroundingspaces - 1) * space) // (space + finger)) # type: ignore
# shrink surrounding space up to half a thickness each side
if fingers == 0 and length > finger + 1.0 * self.settings.thickness: # type: ignore
fingers = 1
if not finger:
fingers = 0
if bedBolts:
fingers = bedBolts.numFingers(fingers)
leftover = length - fingers * (space + finger) + space
if fingers <= 0:
fingers = 0
leftover = length
return fingers, leftover
[docs]
def fingerLength(self, angle: float) -> tuple[float, float]:
# sharp corners
if angle >= 90 or angle <= -90:
return self.settings.thickness + self.settings.extra_length, 0.0 # type: ignore
# inner blunt corners
if angle < 0:
return (math.sin(math.radians(-angle)) * self.settings.thickness + self.settings.extra_length), 0 # type: ignore
# 0 to 90 (blunt corners)
a = 90 - (180 - angle) / 2.0
fingerlength = self.settings.thickness * math.tan(math.radians(a)) # type: ignore
b = 90 - 2 * a
spacerecess = -math.sin(math.radians(b)) * fingerlength
return fingerlength + self.settings.extra_length, spacerecess # type: ignore
[docs]
class FingerJointEdge(BaseEdge, FingerJointBase):
"""Finger joint edge """
char = 'f'
description = "Finger Joint"
positive = True
[docs]
def draw_finger(self, f, h, style, positive: bool = True, firsthalf: bool = True) -> None:
t = self.settings.thickness
if positive:
if style == "springs":
self.polyline(
0, -90, 0.8 * h, (90, 0.2 * h),
0.1 * h, 90, 0.9 * h, -180, 0.9 * h, 90,
f - 0.6 * h,
90, 0.9 * h, -180, 0.9 * h, 90, 0.1 * h,
(90, 0.2 * h), 0.8 * h, -90)
elif style == "barbs":
n = int((h - 0.1 * t) // (0.3 * t))
a = math.degrees(math.atan(0.5))
l = 5 ** 0.5
poly = [h - n * 0.3 * t] + \
([-45, 0.1 * 2 ** 0.5 * t, 45 + a, l * 0.1 * t, -a, 0] * n)
self.polyline(
0, -90, *poly, 90, f, 90, *reversed(poly), -90
)
elif style == "snap" and f > 1.9 * t:
a12 = math.degrees(math.atan(0.5))
l12 = t / math.cos(math.radians(a12))
d = 4 * t
d2 = d + 1 * t
a = math.degrees(math.atan((0.5 * t) / (h + d2)))
l = (h + d2) / math.cos(math.radians(a))
poly = [0, 90, d, -180, d + h, -90, 0.5 * t, 90 + a12, l12, 90 - a12,
0.5 * t, 90 - a, l, +a, 0, (-180, 0.1 * t), h + d2, 90, f - 1.7 * t, 90 - a12, l12, a12, h, -90, 0]
if firsthalf:
poly = list(reversed(poly))
self.polyline(*poly)
else:
self.polyline(0, -90, h, 90, f, 90, h, -90)
else:
self.polyline(0, 90, h, -90, f, -90, h, 90)
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
positive = self.positive
t = self.settings.thickness
s, f = self.settings.space, self.settings.finger
thickness = self.settings.thickness
style = self.settings.style
play = self.settings.play
fingers, leftover = self.calcFingers(length, bedBolts)
# not enough space for normal fingers - use small rectangular one
if (fingers == 0 and f and
leftover > 0.75 * thickness and leftover > 4 * play):
fingers = 1
f = leftover = leftover / 2.0
bedBolts = None
style = "rectangular"
if not positive:
f += play
s -= play
leftover -= play
self.edge(leftover / 2.0, tabs=1)
l1, l2 = self.fingerLength(self.settings.angle)
h = l1 - l2
d = (bedBoltSettings or self.bedBoltSettings)[0]
for i in range(fingers):
if i != 0:
if not positive and bedBolts and bedBolts.drawBolt(i):
self.hole(0.5 * s,
0.5 * self.settings.thickness, 0.5 * d)
if positive and bedBolts and bedBolts.drawBolt(i):
self.bedBoltHole(s, bedBoltSettings)
else:
self.edge(s)
self.draw_finger(f, h, style,
positive, i < fingers // 2)
self.edge(leftover / 2.0, tabs=1)
[docs]
def margin(self) -> float:
""" """
widths = self.fingerLength(self.settings.angle)
if self.positive:
if self.settings.style == "snap":
return widths[0] - widths[1] + self.settings.thickness
return widths[0] - widths[1]
return 0.0
[docs]
def startwidth(self) -> float:
widths = self.fingerLength(self.settings.angle)
return widths[self.positive]
[docs]
class FingerJointEdgeCounterPart(FingerJointEdge):
"""Finger joint edge - other side"""
char = 'F'
description = "Finger Joint (opposing side)"
positive = False
[docs]
class FingerHoles(FingerJointBase):
"""Hole matching a finger joint edge"""
def __init__(self, boxes, settings) -> None:
self.boxes = boxes
self.ctx = boxes.ctx
self.settings = settings
def __call__(self, x, y, length, angle=90, bedBolts=None, bedBoltSettings=None):
"""
Draw holes for a matching finger joint edge
:param x: x position
:param y: y position
:param length: length of matching edge
:param angle: (Default value = 90)
:param bedBolts: (Default value = None)
:param bedBoltSettings: (Default value = None)
"""
with self.boxes.saved_context():
self.boxes.moveTo(x, y, angle)
s, f = self.settings.space, self.settings.finger
p = self.settings.play
b = self.boxes.burn
fingers, leftover = self.calcFingers(length, bedBolts)
# not enough space for normal fingers - use small rectangular one
if (fingers == 0 and f and
leftover > 0.75 * self.settings.thickness and leftover > 4 * p):
fingers = 1
f = leftover = leftover / 2.0
bedBolts = None
if self.boxes.debug:
self.ctx.rectangle(b, -self.settings.width / 2 + b,
length - 2 * b, self.settings.width - 2 * b)
for i in range(fingers):
pos = leftover / 2.0 + i * (s + f)
if bedBolts and bedBolts.drawBolt(i):
d = (bedBoltSettings or self.boxes.bedBoltSettings)[0]
self.boxes.hole(pos - 0.5 * s, 0, d * 0.5)
self.boxes.rectangularHole(pos + 0.5 * f, 0,
f + p, self.settings.width + p)
[docs]
class FingerHoleEdge(BaseEdge):
"""Edge with holes for a parallel finger joint"""
char = 'h'
description = "Edge (parallel Finger Joint Holes)"
def __init__(self, boxes, fingerHoles=None, **kw) -> None:
settings = None
if isinstance(fingerHoles, Settings):
settings = fingerHoles
fingerHoles = FingerHoles(boxes, settings)
super().__init__(boxes, settings, **kw)
self.fingerHoles = fingerHoles or boxes.fingerHolesAt
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
dist = self.fingerHoles.settings.edge_width
with self.saved_context():
self.fingerHoles(
0, self.burn + dist + self.settings.thickness / 2, length, 0,
bedBolts=bedBolts, bedBoltSettings=bedBoltSettings)
if self.settings.bottom_lip:
h = self.settings.bottom_lip + \
self.fingerHoles.settings.edge_width
sp = self.boxes.spacing
self.moveTo(-sp / 2, -h - sp)
self.rectangularWall(length - 1.05 * self.boxes.thickness, h)
self.edge(length, tabs=2)
[docs]
def startwidth(self) -> float:
""" """
return self.fingerHoles.settings.edge_width + self.settings.thickness
[docs]
def margin(self) -> float:
if self.settings.bottom_lip:
return self.settings.bottom_lip + self.fingerHoles.settings.edge_width + self.boxes.spacing
return 0.0
[docs]
class CrossingFingerHoleEdge(Edge):
"""Edge with holes for finger joints 90° above"""
description = "Edge (orthogonal Finger Joint Holes)"
char = '|'
def __init__(self, boxes, height, fingerHoles=None, outset: float = 0.0, **kw) -> None:
super().__init__(boxes, None, **kw)
self.fingerHoles = fingerHoles or boxes.fingerHolesAt
self.height = height
self.outset = outset
def __call__(self, length, **kw):
self.fingerHoles(length / 2.0, self.outset + self.burn, self.height)
super().__call__(length)
[docs]
def startwidth(self) -> float:
return self.outset
#############################################################################
#### Stackable Joints
#############################################################################
[docs]
class StackableSettings(Settings):
"""Settings for Stackable Edges
Values:
* absolute_params
* angle : 60 : inside angle of the feet
* relative (in multiples of thickness)
* height : 2.0 : height of the feet (multiples of thickness)
* width : 4.0 : width of the feet (multiples of thickness)
* holedistance : 1.0 : distance from finger holes to bottom edge (multiples of thickness)
* bottom_stabilizers : 0.0 : height of strips to be glued to the inside of bottom edges (multiples of thickness)
"""
absolute_params = {
"angle": 60,
}
relative_params = {
"height": 2.0,
"width": 4.0,
"holedistance": 1.0,
"bottom_stabilizers": 0.0,
}
[docs]
def checkValues(self) -> None:
if self.angle < 20:
raise ValueError("StackableSettings: 'angle' is too small. Use value >= 20")
if self.angle > 260:
raise ValueError("StackableSettings: 'angle' is too big. Use value < 260")
[docs]
def edgeObjects(self, boxes, chars: str = "sSšŠ", add: bool = True, fingersettings=None):
fingersettings = fingersettings or boxes.edges["f"].settings
edges = [StackableEdge(boxes, self, fingersettings),
StackableEdgeTop(boxes, self, fingersettings),
StackableFeet(boxes, self, fingersettings),
StackableHoleEdgeTop(boxes, self, fingersettings),
]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class StackableBaseEdge(BaseEdge):
"""Edge for having stackable Boxes. The Edge creates feet on the bottom
and has matching recesses on the top corners."""
char = "s"
description = "Abstract Stackable class"
bottom = True
def __init__(self, boxes, settings, fingerjointsettings) -> None:
super().__init__(boxes, settings)
self.fingerjointsettings = fingerjointsettings
def __call__(self, length, **kw):
s = self.settings
r = s.height / 2.0 / (1 - math.cos(math.radians(s.angle)))
l = r * math.sin(math.radians(s.angle))
p = 1 if self.bottom else -1
if self.bottom and s.bottom_stabilizers:
with self.saved_context():
sp = self.boxes.spacing
self.moveTo(-sp / 2)
self.rectangularWall(length - 1.05 * self.boxes.thickness,
s.bottom_stabilizers, move="down")
self.boxes.edge(s.width, tabs=1)
self.boxes.corner(p * s.angle, r)
self.boxes.corner(-p * s.angle, r)
self.boxes.edge(length - 2 * s.width - 4 * l)
self.boxes.corner(-p * s.angle, r)
self.boxes.corner(p * s.angle, r)
self.boxes.edge(s.width, tabs=1)
def _height(self):
return self.settings.height + self.settings.holedistance + self.settings.thickness
[docs]
def startwidth(self) -> float:
return self._height() if self.bottom else 0
[docs]
def margin(self) -> float:
if self.bottom:
if self.settings.bottom_stabilizers:
return self.settings.bottom_stabilizers + self.boxes.spacing
else:
return 0
else:
return self.settings.height
[docs]
class StackableEdge(StackableBaseEdge):
"""Edge for having stackable Boxes. The Edge creates feet on the bottom
and has matching recesses on the top corners."""
char = "s"
description = "Stackable (bottom, finger joint holes)"
def __call__(self, length, **kw):
s = self.settings
self.boxes.fingerHolesAt(
0,
s.height + s.holedistance + 0.5 * self.boxes.thickness,
length, 0)
super().__call__(length, **kw)
[docs]
class StackableEdgeTop(StackableBaseEdge):
char = "S"
description = "Stackable (top)"
bottom = False
[docs]
class StackableFeet(StackableBaseEdge):
char = "š"
description = "Stackable feet (bottom)"
def _height(self):
return self.settings.height
[docs]
class StackableHoleEdgeTop(StackableBaseEdge):
char = "Š"
description = "Stackable edge with finger holes (top)"
bottom = False
[docs]
def startwidth(self) -> float:
return self.settings.thickness + self.settings.holedistance
def __call__(self, length, **kw):
s = self.settings
self.boxes.fingerHolesAt(
0,
s.holedistance + 0.5 * self.boxes.thickness,
length, 0)
super().__call__(length, **kw)
#############################################################################
#### Hinges
#############################################################################
[docs]
class HingeSettings(Settings):
"""Settings for Hinges and HingePins
Values:
* absolute_params
* outset : False : have lid overlap at the sides (similar to OutSetEdge)
* pinwidth : 1.0 : set to lower value to get disks surrounding the pins
* grip_percentage" : 0 : percentage of the lid that should get grips
* relative (in multiples of thickness)
* hingestrength : 1 : thickness of the arc holding the pin in place (multiples of thickness)
* axle : 2 : diameter of the pin hole (multiples of thickness)
* grip_length : 0 : fixed length of the grips on he lids (multiples of thickness)
"""
absolute_params = {
"outset": False,
"pinwidth": 0.5,
"grip_percentage": 0,
}
relative_params = {
"hingestrength": 1, # 1.5-0.5*2**0.5,
"axle": 2.0,
"grip_length": 0,
}
style = "outset" # "outset", "flush", "flush_inset"
[docs]
def checkValues(self) -> None:
if self.axle / self.thickness < 0.1:
raise ValueError("HingeSettings: 'axle' need to be at least 0.1 strong")
[docs]
def edgeObjects(self, boxes, chars: str = "iIjJkK", add: bool = True):
edges = [
Hinge(boxes, self, 1),
HingePin(boxes, self, 1),
Hinge(boxes, self, 2),
HingePin(boxes, self, 2),
Hinge(boxes, self, 3),
HingePin(boxes, self, 3),
]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class Hinge(BaseEdge):
char = 'i'
description = "Straight edge with hinge eye"
def __init__(self, boxes, settings=None, layout: int = 1) -> None:
super().__init__(boxes, settings)
if not (0 < layout <= 3):
raise ValueError("layout must be 1, 2 or 3 (got %i)" % layout)
self.layout = layout
self.char = "eijk"[layout]
self.description = self.description + ('', ' (start)', ' (end)', ' (both ends)')[layout]
[docs]
def margin(self) -> float:
t: float = self.settings.thickness
if self.settings.style == "outset":
r = 0.5 * self.settings.axle
alpha = math.degrees(math.asin(0.5 * t / r))
pos = math.cos(math.radians(alpha)) * r
return 1.5 * t + pos
else: # flush
return 0.5 * t + 0.5 * self.settings.axle + self.settings.hingestrength
[docs]
def outset(self, _reversed: bool = False) -> None:
t: float = self.settings.thickness
r = 0.5 * self.settings.axle
alpha = math.degrees(math.asin(0.5 * t / r))
pinl = (self.settings.axle ** 2 - self.settings.thickness ** 2) ** 0.5 * self.settings.pinwidth
pos = math.cos(math.radians(alpha)) * r
hinge = (
0.,
90. - alpha, 0.,
(-360., r), 0.,
90. + alpha,
t,
90.,
0.5 * t,
(180., t + pos), 0.,
(-90., 0.5 * t), 0.
)
if _reversed:
hinge = reversed(hinge) # type: ignore
self.polyline(*hinge)
self.boxes.rectangularHole(-pos, -0.5 * t, pinl, self.settings.thickness)
else:
self.boxes.rectangularHole(pos, -0.5 * t, pinl, self.settings.thickness)
self.polyline(*hinge)
[docs]
def outsetlen(self) -> float:
t = self.settings.thickness
r = 0.5 * self.settings.axle
alpha = math.degrees(math.asin(0.5 * t / r))
pos = math.cos(math.radians(alpha)) * r
return 2.0 * pos + 1.5 * t
[docs]
def flush(self, _reversed: bool = False) -> None:
t = self.settings.thickness
hinge = (
0., -90.,
0.5 * t,
(180., 0.5 * self.settings.axle + self.settings.hingestrength), 0.,
(-90., 0.5 * t), 0.
)
pos = 0.5 * self.settings.axle + self.settings.hingestrength
pinl = (self.settings.axle ** 2 - self.settings.thickness ** 2) ** 0.5 * self.settings.pinwidth
if _reversed:
hinge = reversed(hinge) # type: ignore
self.hole(0.5 * t + pos, -0.5 * t, 0.5 * self.settings.axle)
self.boxes.rectangularHole(0.5 * t + pos, -0.5 * t, pinl, self.settings.thickness)
else:
self.hole(pos, -0.5 * t, 0.5 * self.settings.axle)
self.boxes.rectangularHole(pos, -0.5 * t, pinl, self.settings.thickness)
self.polyline(*hinge)
[docs]
def flushlen(self) -> float:
return self.settings.axle + 2.0 * self.settings.hingestrength + 0.5 * self.settings.thickness
def __call__(self, l, **kw):
hlen = getattr(self, self.settings.style + 'len', self.flushlen)()
if self.layout in (1, 3):
getattr(self, self.settings.style, self.flush)()
self.edge(l - (self.layout & 1) * hlen - bool(self.layout & 2) * hlen,
tabs=2)
if self.layout in (2, 3):
getattr(self, self.settings.style, self.flush)(True)
[docs]
class HingePin(BaseEdge):
char = 'I'
description = "Edge with hinge pin"
def __init__(self, boxes, settings=None, layout: int = 1) -> None:
super().__init__(boxes, settings)
if not (0 < layout <= 3):
raise ValueError("layout must be 1, 2 or 3 (got %i)" % layout)
self.layout = layout
self.char = "EIJK"[layout]
self.description = self.description + ('', ' (start)', ' (end)', ' (both ends)')[layout]
[docs]
def startwidth(self) -> float:
if self.layout & 1:
return 0.0
return self.settings.outset * self.boxes.thickness
[docs]
def endwidth(self) -> float:
if self.layout & 2:
return 0.0
return self.settings.outset * self.boxes.thickness
[docs]
def margin(self) -> float:
if self.settings.outset and (
self.settings.grip_percentage > 0.0 or
self.settings.grip_length > 0.0 ):
return self.settings.thickness + self.boxes.edges['g'].margin()
else:
return self.settings.thickness
[docs]
def outset(self, _reversed: bool = False) -> None:
t: float = self.settings.thickness
r = 0.5 * self.settings.axle
alpha = math.degrees(math.asin(0.5 * t / r))
pos = math.cos(math.radians(alpha)) * r
pinl = (self.settings.axle ** 2 - self.settings.thickness ** 2) ** 0.5 * self.settings.pinwidth
pin = (pos - 0.5 * pinl, -90.,
t, 90.,
pinl,
90.,
t,
-90.)
if self.settings.outset:
pin += ( # type: ignore
pos - 0.5 * pinl + 1.5 * t,
-90.,
t,
90.,
0.,
)
else:
pin += (pos - 0.5 * pinl,) # type: ignore
if _reversed:
pin = reversed(pin) # type: ignore
self.polyline(*pin)
[docs]
def outsetlen(self):
t = self.settings.thickness
r = 0.5 * self.settings.axle
alpha = math.degrees(math.asin(0.5 * t / r))
pos = math.cos(math.radians(alpha)) * r
if self.settings.outset:
return 2 * pos + 1.5 * self.settings.thickness
return 2 * pos
[docs]
def flush(self, _reversed: bool = False) -> None:
t: float = self.settings.thickness
pinl = (self.settings.axle ** 2 - t ** 2) ** 0.5 * self.settings.pinwidth
d = d1 = (self.settings.axle - pinl) / 2.0
if self.settings.style == "flush_inset":
d1 -= self.settings.thickness
pin = (self.settings.hingestrength + d1, -90.,
t, 90.,
pinl,
90.,
t,
-90., d)
if self.settings.outset:
pin += ( # type: ignore
0.,
self.settings.hingestrength + 0.5 * t,
-90.,
t,
90.,
0.,
)
if _reversed:
pin = reversed(pin) # type: ignore
self.polyline(*pin)
[docs]
def flushlen(self):
l = self.settings.hingestrength + self.settings.axle
if self.settings.style == "flush_inset":
l -= self.settings.thickness
if self.settings.outset:
l += self.settings.hingestrength + 0.5 * self.settings.thickness
return l
def __call__(self, l, **kw):
plen = getattr(self, self.settings.style + 'len', self.flushlen)()
glen = l * self.settings.grip_percentage / 100 + \
self.settings.grip_length
if not self.settings.outset:
glen = 0.0
glen = min(glen, l - plen)
if self.layout == 3:
getattr(self, self.settings.style, self.flush)()
self.edge(l - 2 * plen, tabs=2)
getattr(self, self.settings.style, self.flush)(True)
elif self.layout == 1:
getattr(self, self.settings.style, self.flush)()
self.edge(l - plen - glen, tabs=2)
self.edges['g'](glen)
else: # self.layout == 2
self.edges['g'](glen)
self.edge(l - plen - glen, tabs=2)
getattr(self, self.settings.style, self.flush)(True)
#############################################################################
#### Chest Hinge
#############################################################################
[docs]
class ChestHingeSettings(Settings):
"""Settings for Chest Hinges
Values:
* relative (in multiples of thickness)
* pin_height : 2.0 : radius of the disc rotating in the hinge (multiples of thickness)
* hinge_strength : 1.0 : thickness of the arc holding the pin in place (multiples of thickness)
* absolute
* finger_joints_on_box : False : whether to include finger joints on the edge with the box
* finger_joints_on_lid : False : whether to include finger joints on the edge with the lid
"""
relative_params = {
"pin_height": 2.0,
"hinge_strength": 1.0,
"play": 0.1,
}
absolute_params = {
"finger_joints_on_box": False,
"finger_joints_on_lid": False,
}
[docs]
def checkValues(self) -> None:
if self.pin_height / self.thickness < 1.2:
raise ValueError("ChestHingeSettings: 'pin_height' must be >= 1.2")
[docs]
def pinheight(self):
return ((0.9 * self.pin_height) ** 2 - self.thickness ** 2) ** 0.5
[docs]
def edgeObjects(self, boxes, chars: str = "oOpPqQ", add: bool = True):
edges = [
ChestHinge(boxes, self),
ChestHinge(boxes, self, True),
ChestHingeTop(boxes, self),
ChestHingeTop(boxes, self, True),
ChestHingePin(boxes, self),
ChestHingeFront(boxes, self),
]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class ChestHinge(BaseEdge):
description = "Edge with chest hinge"
char = "o"
def __init__(self, boxes, settings=None, reversed: bool = False) -> None:
super().__init__(boxes, settings)
self.reversed = reversed
self.char = "oO"[reversed]
self.description = self.description + (' (start)', ' (end)')[reversed]
def __call__(self, l, **kw):
t = self.settings.thickness
p = self.settings.pin_height
s = self.settings.hinge_strength
pinh = self.settings.pinheight()
if self.reversed:
self.hole(l + t, 0, p, tabs=4)
self.rectangularHole(l + 0.5 * t, -0.5 * pinh, t, pinh)
else:
self.hole(-t, -s - p, p, tabs=4)
self.rectangularHole(-0.5 * t, -s - p - 0.5 * pinh, t, pinh)
if self.settings.finger_joints_on_box:
final_segment = t - s
draw_rest_of_edge = lambda: self.edges["F"](l - p)
else:
final_segment = l + t - p - s
draw_rest_of_edge = lambda: None
poly = (0, -180, t, (270, p + s), 0, -90, final_segment)
if self.reversed:
draw_rest_of_edge()
self.polyline(*reversed(poly))
else:
self.polyline(*poly)
draw_rest_of_edge()
[docs]
def margin(self) -> float:
if self.reversed:
return 0.0
return 1 * (self.settings.pin_height + self.settings.hinge_strength)
[docs]
def startwidth(self) -> float:
if self.reversed:
return self.settings.pin_height + self.settings.hinge_strength
return 0.0
[docs]
def endwidth(self) -> float:
if self.reversed:
return 0.0
return self.settings.pin_height + self.settings.hinge_strength
[docs]
class ChestHingeTop(ChestHinge):
"""Edge above a chest hinge"""
char = "p"
def __init__(self, boxes, settings=None, reversed: bool = False) -> None:
super().__init__(boxes, settings)
self.reversed = reversed
self.char = "oO"[reversed]
self.description = self.description + (' (start)', ' (end)')[reversed]
def __call__(self, l, **kw):
t = self.settings.thickness
p = self.settings.pin_height
s = self.settings.hinge_strength
play = self.settings.play
if self.settings.finger_joints_on_lid:
final_segment = t - s - play
draw_rest_of_edge = lambda: self.edges["F"](l - p)
else:
final_segment = l + t - p - s - play
draw_rest_of_edge = lambda: None
poly = (0, -180, t, -180, 0, (-90, p + s + play), 0, 90, final_segment)
if self.reversed:
draw_rest_of_edge()
self.polyline(*reversed(poly))
else:
self.polyline(*poly)
draw_rest_of_edge()
[docs]
def startwidth(self) -> float:
if self.reversed:
return self.settings.play + self.settings.pin_height + self.settings.hinge_strength
return 0.0
[docs]
def endwidth(self) -> float:
if self.reversed:
return 0.0
return self.settings.play + self.settings.pin_height + self.settings.hinge_strength
[docs]
def margin(self) -> float:
if self.reversed:
return 0.0
return 1 * (self.settings.play + self.settings.pin_height + self.settings.hinge_strength)
[docs]
class ChestHingePin(BaseEdge):
description = "Edge with pins for an chest hinge"
char = "q"
def __call__(self, l, **kw):
t = self.settings.thickness
p = self.settings.pin_height
s = self.settings.hinge_strength
pinh = self.settings.pinheight()
if self.settings.finger_joints_on_lid:
middle_segment = [0]
draw_rest_of_edge = lambda: (self.edge(t), self.edges["F"](l), self.edge(t))
else:
middle_segment = [l + 2 * t, ]
draw_rest_of_edge = lambda: None
poly = [0, -90, s + p - pinh, -90, t, 90, pinh, 90, ]
self.polyline(*poly)
draw_rest_of_edge()
self.polyline(*(middle_segment + list(reversed(poly))))
[docs]
def margin(self) -> float:
return (self.settings.pin_height + self.settings.hinge_strength)
[docs]
class ChestHingeFront(Edge):
description = "Edge opposing a chest hinge"
char = "Q"
[docs]
def startwidth(self) -> float:
return self.settings.pin_height + self.settings.hinge_strength
#############################################################################
#### Cabinet Hinge
#############################################################################
[docs]
class CabinetHingeSettings(Settings):
"""Settings for Cabinet Hinges
Values:
* absolute_params
* bore : 3.2 : diameter of the pin hole in mm
* eyes_per_hinge : 5 : pieces per hinge
* hinges : 2 : number of hinges per edge
* style : inside : style of hinge used
* relative (in multiples of thickness)
* eye : 1.5 : radius of the eye (multiples of thickness)
* play : 0.05 : space between eyes (multiples of thickness)
* spacing : 2.0 : minimum space around the hinge (multiples of thickness)
"""
absolute_params = {
"bore": 3.2,
"eyes_per_hinge": 5,
"hinges": 2,
"style": ("inside", "outside"),
}
relative_params = {
"eye": 1.5,
"play": 0.05,
"spacing": 2.0,
}
[docs]
def edgeObjects(self, boxes, chars: str = "uUvV", add: bool = True):
edges = [CabinetHingeEdge(boxes, self),
CabinetHingeEdge(boxes, self, top=True),
CabinetHingeEdge(boxes, self, angled=True),
CabinetHingeEdge(boxes, self, top=True, angled=True),
]
for e, c in zip(edges, chars):
e.char = c
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class CabinetHingeEdge(BaseEdge):
"""Edge with cabinet hinges"""
char = "u"
description = "Edge with cabinet hinges"
def __init__(self, boxes, settings=None, top: bool = False, angled: bool = False) -> None:
super().__init__(boxes, settings)
self.top = top
self.angled = angled
self.char = "uUvV"[bool(top) + 2 * bool(angled)]
[docs]
def startwidth(self) -> float:
return self.settings.thickness if self.top and self.angled else 0.0
def __poly(self):
n = self.settings.eyes_per_hinge
p = self.settings.play
e = self.settings.eye
t = self.settings.thickness
spacing = self.settings.spacing
if self.settings.style == "outside" and self.angled:
e = t
elif self.angled and not self.top:
# move hinge up to leave space for lid
e -= t
if self.top:
# start with space
poly = [spacing, 90, e + p]
else:
# start with hinge eye
poly = [spacing + p, 90, e + p, 0]
for i in range(n):
if (i % 2) ^ self.top:
# space
if i == 0:
poly += [-90, t + 2 * p, 90]
else:
poly += [90, t + 2 * p, 90]
else:
# hinge eye
poly += [t - p, -90, t, -90, t - p]
if (n % 2) ^ self.top:
# stopped with hinge eye
poly += [0, e + p, 90, p + spacing]
else:
# stopped with space
poly[-1:] = [-90, e + p, 90, 0 + spacing]
width = (t + p) * n + p + 2 * spacing
return poly, width
def __call__(self, l, **kw):
n = self.settings.eyes_per_hinge
p = self.settings.play
e = self.settings.eye
t = self.settings.thickness
hn = self.settings.hinges
poly, width = self.__poly()
if self.settings.style == "outside" and self.angled:
e = t
elif self.angled and not self.top:
# move hinge up to leave space for lid
e -= t
hn = min(hn, int(l // width))
if hn == 1:
self.edge((l - width) / 2, tabs=2)
for j in range(hn):
for i in range(n):
if not (i % 2) ^ self.top:
self.rectangularHole(self.settings.spacing + 0.5 * t + p + i * (t + p), e + 2.5 * t, t, t)
self.polyline(*poly)
if j < (hn - 1):
self.edge((l - hn * width) / (hn - 1), tabs=2)
if hn == 1:
self.edge((l - width) / 2, tabs=2)
[docs]
def parts(self, move=None) -> None:
e, b = self.settings.eye, self.settings.bore
t = self.settings.thickness
n = self.settings.eyes_per_hinge * self.settings.hinges
pairs = n // 2 + 2 * (n % 2)
if self.settings.style == "outside":
th = 2 * e + 4 * t
tw = n * (max(3 * t, 2 * e) + self.boxes.spacing)
else:
th = 4 * e + 3 * t + self.boxes.spacing
tw = max(e, 2 * t) * pairs
if self.move(tw, th, move, True, label="hinges"):
return
if self.settings.style == "outside":
ax = max(t / 2, e - t)
self.moveTo(t + ax)
for i in range(n):
if self.angled:
if i > n // 2:
l = 4 * t + ax
else:
l = 5 * t + ax
else:
l = 3 * t + e
self.hole(0, e, b / 2.0)
da = math.asin((t - ax) / e)
dad = math.degrees(da)
dy = e * (1 - math.cos(da))
self.polyline(0, (180 - dad, e), 0, (-90 + dad), dy + l - e, (90, t))
self.polyline(0, 90, t, -90, t, 90, t, 90, t, -90, t, -90, t,
90, t, 90, (ax + t) - e, -90, l - 3 * t, (90, e))
self.moveTo(2 * max(e, 1.5 * t) + self.boxes.spacing)
self.move(tw, th, move, label="hinges")
return
if e <= 2 * t:
if self.angled:
corner = [2 * e - t, (90, 2 * t - e), 0, -90, t, (90, e)]
else:
corner = [2 * e, (90, 2 * t)]
else:
a = math.asin(2 * t / e)
ang = math.degrees(a)
corner = [e * (1 - math.cos(a)) + 2 * t, -90 + ang, 0, (180 - ang, e)]
self.moveTo(max(e, 2 * t))
for i in range(n):
self.hole(0, e, b / 2.0)
self.polyline(*[0, (180, e), 0, -90, t, 90, t, -90, t, -90, t, 90, t, 90, t, (90, t)] + corner)
self.moveTo(self.boxes.spacing, 4 * e + 3 * t + self.boxes.spacing, 180)
if i % 2:
self.moveTo(2 * max(e, 2 * t) + 2 * self.boxes.spacing)
self.move(th, tw, move, label="hinges")
#############################################################################
#### Slide-on lid
#############################################################################
[docs]
class SlideOnLidSettings(FingerJointSettings):
"""Settings for Slide-on Lids
Note that edge_width below also determines how much the sides extend above the lid.
Values:
* absolute_params
* second_pin : True : additional pin for better positioning
* spring : "both" : position(s) of the extra locking springs in the lid
* hole_width : 0 : width of the "finger hole" in mm
"""
__doc__ += FingerJointSettings.__doc__ or ""
absolute_params = FingerJointSettings.absolute_params.copy()
relative_params = FingerJointSettings.relative_params.copy()
relative_params.update({
"play": 0.05,
"finger": 3.0,
"space": 2.0,
})
absolute_params.update({
"second_pin": True,
"spring": ("both", "none", "left", "right"),
"hole_width": 0
})
[docs]
def edgeObjects(self, boxes, chars=None, add: bool = True):
edges = [LidEdge(boxes, self),
LidHoleEdge(boxes, self),
LidRight(boxes, self),
LidLeft(boxes, self),
LidSideRight(boxes, self),
LidSideLeft(boxes, self),
]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class LidEdge(FingerJointEdge):
char = "l"
description = "Edge for slide on lid (back)"
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
hole_width = self.settings.hole_width
if hole_width > 0:
super().__call__((length - hole_width) / 2)
GroovedEdgeBase.groove_arc(self, hole_width)
super().__call__((length - hole_width) / 2)
else:
super().__call__(length)
[docs]
class LidHoleEdge(FingerHoleEdge):
char = "L"
description = "Edge for slide on lid (box back)"
def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw) -> None:
hole_width = self.settings.hole_width
if hole_width > 0:
super().__call__((length - hole_width) / 2)
self.edge(hole_width)
super().__call__((length - hole_width) / 2)
else:
super().__call__(length)
[docs]
class LidRight(BaseEdge):
char = "n"
description = "Edge for slide on lid (right)"
rightside = True
def __call__(self, length, **kw):
t = self.boxes.thickness
if self.rightside:
spring = self.settings.spring in ("right", "both")
else:
spring = self.settings.spring in ("left", "both")
if spring:
l = min(6 * t, length - 2 * t)
a = 30
sqt = 0.4 * t / math.cos(math.radians(a))
sw = 0.5 * t
p = [0, 90, 1.5 * t + sw, -90, l, (-180, 0.25 * t), l - 0.2 * t, 90, sw, 90 - a, sqt, 2 * a, sqt, -a, length - t]
else:
p = [t, 90, t, -90, length - t]
pin = self.settings.second_pin
if pin:
pinl = 2 * t
p[-1:] = [length - 2 * t - pinl, -90, t, 90, pinl, 90, t, -90, t]
if not self.rightside:
p = list(reversed(p))
self.polyline(*p)
[docs]
def startwidth(self) -> float:
if self.rightside: # or self.settings.second_pin:
return self.boxes.thickness
return 0.0
[docs]
def endwidth(self) -> float:
if not self.rightside: # or self.settings.second_pin:
return self.boxes.thickness
return 0.0
[docs]
def margin(self) -> float:
if not self.rightside: # and not self.settings.second_pin:
return self.boxes.thickness
return 0.0
[docs]
class LidLeft(LidRight):
char = "m"
description = "Edge for slide on lid (left)"
rightside = False
[docs]
class LidSideRight(BaseEdge):
char = "N"
description = "Edge for slide on lid (box right)"
rightside = True
def __call__(self, length, **kw):
t = self.boxes.thickness
s = self.settings.play
pin = self.settings.second_pin
edge_width = self.settings.edge_width
r = edge_width / 3
if self.rightside:
spring = self.settings.spring in ("right", "both")
else:
spring = self.settings.spring in ("left", "both")
if spring:
p = [s, -90, t + s, -90, t + s, 90, edge_width - s / 2, 90, length + t]
else:
p = [t + s, -90, t + s, -90, 2 * t + s, 90, edge_width - s / 2, 90, length + t]
if pin:
pinl = 2 * t
p[-1:] = [p[-1] - 1.5 * t - 2 * pinl - r, (90, r), edge_width + t + s / 2 - r, -90, 2 * pinl + s + 0.5 * t, -90, t + s, -90,
pinl - r, (90, r), edge_width - s / 2 - 2 * r, (90, r), pinl + t - s - r]
holex = 0.6 * t
holey = -0.5 * t + self.burn - s / 2
if self.rightside:
p = list(reversed(p))
holex = length - holex
holey = edge_width + 0.5 * t + self.burn
if spring:
self.rectangularHole(holex, holey, 0.4 * t, t + 2 * s)
self.polyline(*p)
[docs]
def startwidth(self) -> float:
return self.boxes.thickness + self.settings.edge_width if self.rightside else -self.settings.play / 2
[docs]
def endwidth(self) -> float:
return self.boxes.thickness + self.settings.edge_width if not self.rightside else -self.settings.play / 2
[docs]
def margin(self) -> float:
return self.boxes.thickness + self.settings.edge_width + self.settings.play / 2 if not self.rightside else 0.0
[docs]
class LidSideLeft(LidSideRight):
char = "M"
description = "Edge for slide on lid (box left)"
rightside = False
#############################################################################
#### Click Joints
#############################################################################
[docs]
class ClickSettings(Settings):
"""Settings for Click-on Lids
Values:
* absolute_params
* angle : 5.0 : angle of the hooks bending outward
* relative (in multiples of thickness)
* depth : 3.0 : length of the hooks (multiples of thickness)
* bottom_radius : 0.1 : radius at the bottom (multiples of thickness)
"""
absolute_params = {
"angle": 5.0,
}
relative_params = {
"depth": 3.0,
"bottom_radius": 0.1,
}
[docs]
def edgeObjects(self, boxes, chars: str = "cC", add: bool = True):
edges = [ClickConnector(boxes, self),
ClickEdge(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class ClickConnector(BaseEdge):
char = "c"
description = "Click on (bottom side)"
[docs]
def hook(self, reverse: bool = False) -> None:
t = self.settings.thickness
a = self.settings.angle
d = self.settings.depth
r = self.settings.bottom_radius
c = math.cos(math.radians(a))
s = math.sin(math.radians(a))
p1 = (0, 90 - a, c * d)
p2 = (
d + t,
-90,
t * 0.5,
135,
t * 2 ** 0.5,
135,
d + 2 * t + s * 0.5 * t)
p3 = (c * d - s * c * 0.2 * t, -a, 0)
if not reverse:
self.polyline(*p1)
self.corner(-180, r)
self.polyline(*p2)
self.corner(-180 + 2 * a, r)
self.polyline(*p3)
else:
self.polyline(*reversed(p3))
self.corner(-180 + 2 * a, r)
self.polyline(*reversed(p2))
self.corner(-180, r)
self.polyline(*reversed(p1))
[docs]
def hookWidth(self):
t = self.settings.thickness
a = self.settings.angle
d = self.settings.depth
r = self.settings.bottom_radius
c = math.cos(math.radians(a))
s = math.sin(math.radians(a))
return 2 * s * d * c + 0.5 * c * t + c * 4 * r
[docs]
def hookOffset(self):
a = self.settings.angle
d = self.settings.depth
r = self.settings.bottom_radius
c = math.cos(math.radians(a))
s = math.sin(math.radians(a))
return s * d * c + 2 * r
[docs]
def finger(self, length) -> None:
t = self.settings.thickness
self.polyline(
2 * t,
90,
length,
90,
2 * t,
)
def __call__(self, length, **kw):
t = self.settings.thickness
self.edge(4 * t)
self.hook()
self.finger(2 * t)
self.hook(reverse=True)
self.edge(length - 2 * (6 * t + 2 * self.hookWidth()), tabs=2)
self.hook()
self.finger(2 * t)
self.hook(reverse=True)
self.edge(4 * t)
[docs]
def margin(self) -> float:
return 2 * self.settings.thickness
[docs]
class ClickEdge(ClickConnector):
char = "C"
description = "Click on (top)"
[docs]
def startwidth(self) -> float:
return self.boxes.thickness
[docs]
def margin(self) -> float:
return 0.0
def __call__(self, length, **kw):
t = self.settings.thickness
o = self.hookOffset()
w = self.hookWidth()
p1 = (
4 * t + o,
90,
t,
-90,
2 * (t + w - o),
-90,
t,
90,
0)
self.polyline(*p1)
self.edge(length - 2 * (6 * t + 2 * w) + 2 * o, tabs=2)
self.polyline(*reversed(p1))
#############################################################################
#### Dove Tail Joints
#############################################################################
[docs]
class DoveTailSettings(Settings):
"""Settings for Dove Tail Joints
Values:
* absolute
* angle : 50 : how much should fingers widen (-80 to 80)
* relative (in multiples of thickness)
* size : 3 : from one middle of a dove tail to another (multiples of thickness)
* depth : 1.5 : how far the dove tails stick out of/into the edge (multiples of thickness)
* radius : 0.2 : radius used on all four corners (multiples of thickness)
"""
absolute_params = {
"angle": 50,
}
relative_params = {
"size": 3,
"depth": 1.5,
"radius": 0.2,
}
[docs]
def edgeObjects(self, boxes, chars: str = "dD", add: bool = True):
edges = [DoveTailJoint(boxes, self),
DoveTailJointCounterPart(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class DoveTailJoint(BaseEdge):
"""Edge with dove tail joints """
char = 'd'
description = "Dove Tail Joint"
positive = True
def __call__(self, length, **kw):
s = self.settings
radius = max(s.radius, self.boxes.burn) # no smaller than burn
positive = self.positive
a = s.angle + 90
alpha = 0.5 * math.pi - math.pi * s.angle / 180.0
l1 = radius / math.tan(alpha / 2.0)
diffx = 0.5 * s.depth / math.tan(alpha)
l2 = 0.5 * s.depth / math.sin(alpha)
sections = int((length) // (s.size * 2))
leftover = length - sections * s.size * 2
if sections == 0:
self.edge(length)
return
p = 1 if positive else -1
self.edge((s.size + leftover) / 2.0 + diffx - l1, tabs=1)
for i in range(sections):
self.corner(-1 * p * a, radius)
self.edge(2 * (l2 - l1))
self.corner(p * a, radius)
self.edge(2 * (diffx - l1) + s.size)
self.corner(p * a, radius)
self.edge(2 * (l2 - l1))
self.corner(-1 * p * a, radius)
if i < sections - 1: # all but the last
self.edge(2 * (diffx - l1) + s.size)
self.edge((s.size + leftover) / 2.0 + diffx - l1, tabs=1)
[docs]
def margin(self) -> float:
""" """
return self.settings.depth
[docs]
class DoveTailJointCounterPart(DoveTailJoint):
"""Edge for other side of dove joints """
char = 'D'
description = "Dove Tail Joint (opposing side)"
positive = False
[docs]
def margin(self) -> float:
return 0.0
[docs]
class FlexSettings(Settings):
"""Settings for Flex
Values:
* absolute
* stretch : 1.05 : Hint of how much the flex part should be shortened
* relative (in multiples of thickness)
* distance : 0.5 : width of the pattern perpendicular to the cuts (multiples of thickness)
* connection : 1.0 : width of the gaps in the cuts (multiples of thickness)
* width : 5.0 : width of the pattern in direction of the cuts (multiples of thickness)
"""
relative_params = {
"distance": 0.5,
"connection": 1.0,
"width": 5.0,
}
absolute_params = {
"stretch": 1.05,
}
[docs]
def checkValues(self) -> None:
if self.distance < 0.01:
raise ValueError("Flex Settings: distance parameter must be > 0.01mm")
if self.width < 0.1:
raise ValueError("Flex Settings: width parameter must be > 0.1mm")
[docs]
class FlexEdge(BaseEdge):
"""Edge with flex cuts - use straight edge for the opposing side"""
char = 'X'
description = "Flex cut"
def __call__(self, x, h, **kw):
dist = self.settings.distance
connection = self.settings.connection
width = self.settings.width
burn = self.boxes.burn
h += 2 * burn
lines = int(x // dist)
leftover = x - lines * dist
sections = max(int((h - connection) // width), 1)
sheight = ((h - connection) / sections) - connection
self.ctx.stroke()
for i in range(1, lines):
pos = i * dist + leftover / 2
if i % 2:
self.ctx.move_to(pos, 0)
self.ctx.line_to(pos, connection + sheight)
for j in range((sections - 1) // 2):
self.ctx.move_to(pos, (2 * j + 1) * sheight + (2 * j + 2) * connection)
self.ctx.line_to(pos, (2 * j + 3) * (sheight + connection))
if not sections % 2:
self.ctx.move_to(pos, h - sheight - connection)
self.ctx.line_to(pos, h)
else:
if sections % 2:
self.ctx.move_to(pos, h)
self.ctx.line_to(pos, h - connection - sheight)
for j in range((sections - 1) // 2):
self.ctx.move_to(
pos, h - ((2 * j + 1) * sheight + (2 * j + 2) * connection))
self.ctx.line_to(
pos, h - (2 * j + 3) * (sheight + connection))
else:
for j in range(sections // 2):
self.ctx.move_to(pos,
h - connection - 2 * j * (sheight + connection))
self.ctx.line_to(pos, h - 2 * (j + 1) * (sheight + connection))
self.ctx.stroke()
self.ctx.move_to(0, 0)
self.ctx.line_to(x, 0)
self.ctx.translate(*self.ctx.get_current_point())
[docs]
class GearSettings(Settings):
"""Settings for rack (and pinion) edge
Values:
* absolute_params
* dimension : 3.0 : modulus of the gear (in mm)
* angle : 20.0 : pressure angle
* profile_shift : 20.0 : Profile shift
* clearance : 0.0 : clearance
"""
absolute_params = {
"dimension": 3.0,
"angle": 20.0,
"profile_shift": 20.0,
"clearance": 0.0,
}
relative_params: dict[str, Any] = {}
[docs]
class RackEdge(BaseEdge):
char = "R"
description = "Rack (and pinion) Edge"
def __init__(self, boxes, settings) -> None:
super().__init__(boxes, settings)
self.gear = gears.Gears(boxes)
def __call__(self, length, **kw):
params = self.settings.values.copy()
params["draw_rack"] = True
params["rack_base_height"] = -1E-36
params["rack_teeth_length"] = int(length // (params["dimension"] * math.pi))
params["rack_base_tab"] = (length - (params["rack_teeth_length"]) * params["dimension"] * math.pi) / 2.0
s_tmp = self.boxes.spacing
self.boxes.spacing = 0
self.moveTo(length, 0, 180)
self.gear(move="", **params)
self.moveTo(0, 0, 180)
self.boxes.spacing = s_tmp
[docs]
def margin(self) -> float:
return self.settings.dimension * 1.1
[docs]
class RoundedTriangleEdgeSettings(Settings):
"""Settings for RoundedTriangleEdge
Values:
* absolute_params
* height : 150. : height above the wall
* radius : 30. : radius of top corner
* r_hole : 0. : radius of hole
* relative (in multiples of thickness)
* outset : 0 : extend the triangle along the length of the edge (multiples of thickness)
"""
absolute_params = {
"height": 50.,
"radius": 30.,
"r_hole": 2.,
}
relative_params = {
"outset": 0.,
}
[docs]
def edgeObjects(self, boxes, chars: str = "t", add: bool = True):
edges = [RoundedTriangleEdge(boxes, self),
RoundedTriangleFingerHolesEdge(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
[docs]
class RoundedTriangleEdge(Edge):
"""Makes an 'edge' with a rounded triangular bumpout and
optional hole"""
description = "Triangle for handle"
char = "t"
def __call__(self, length, **kw):
length += 2 * self.settings.outset
r = self.settings.radius
if r > length / 2:
r = length / 2
if length - 2 * r < self.settings.height: # avoid division by zero
angle = 90 - math.degrees(math.atan(
(length - 2 * r) / (2 * self.settings.height)))
l = self.settings.height / math.cos(math.radians(90 - angle))
else:
angle = math.degrees(math.atan(
2 * self.settings.height / (length - 2 * r)))
l = 0.5 * (length - 2 * r) / math.cos(math.radians(angle))
if self.settings.outset:
self.polyline(0, -180, self.settings.outset, 90)
else:
self.corner(-90)
if self.settings.r_hole:
self.hole(self.settings.height, length / 2., self.settings.r_hole)
self.corner(90 - angle, r, tabs=1)
self.edge(l, tabs=1)
self.corner(2 * angle, r, tabs=1)
self.edge(l, tabs=1)
self.corner(90 - angle, r, tabs=1)
if self.settings.outset:
self.polyline(0, 90, self.settings.outset, -180)
else:
self.corner(-90)
[docs]
def margin(self) -> float:
return self.settings.height + self.settings.radius
[docs]
class RoundedTriangleFingerHolesEdge(RoundedTriangleEdge):
char = "T"
[docs]
def startwidth(self) -> float:
return self.settings.thickness
def __call__(self, length, **kw):
self.fingerHolesAt(0, 0.5 * self.settings.thickness, length, 0)
super().__call__(length, **kw)
[docs]
class HandleEdgeSettings(Settings):
"""Settings for HandleEdge
Values:
* absolute_params
* height : 20. : height above the wall in mm
* radius : 10. : radius of corners in mm
* hole_width : "40:40" : width of hole(s) in percentage of maximum hole width (width of edge - (n+1) * material thickness)
* hole_height : 75. : height of hole(s) in percentage of maximum hole height (handle height - 2 * material thickness)
* on_sides : True, : added to side panels if checked, to front and back otherwise (only used with top_edge parameter)
* relative
* outset : 1. : extend the handle along the length of the edge (multiples of thickness)
"""
absolute_params = {
"height": 20.,
"radius": 10.,
"hole_width": "40:40",
"hole_height": 75.,
"on_sides": True,
}
relative_params = {
"outset": 1.,
}
[docs]
def edgeObjects(self, boxes, chars: str = "yY", add: bool = True):
edges = [HandleEdge(boxes, self),
HandleHoleEdge(boxes, self)]
return self._edgeObjects(edges, boxes, chars, add)
# inspiration came from https://www.thingiverse.com/thing:327393
[docs]
class HandleEdge(Edge):
"""Extends an 'edge' by adding a rounded bumpout with optional holes"""
description = "Handle for e.g. a drawer"
char = "y"
extra_height = 0.0
def __call__(self, length, **kw):
length += 2 * self.settings.outset
extra_height = self.extra_height * self.settings.thickness
r = self.settings.radius
if r > length / 2:
r = length / 2
if r > self.settings.height:
r = self.settings.height
widths = argparseSections(self.settings.hole_width)
if self.settings.outset:
self.polyline(0, -180, self.settings.outset, 90)
else:
self.corner(-90)
if self.settings.hole_height and sum(widths) > 0:
if sum(widths) < 100:
slot_offset = ((1 - sum(widths) / 100) * (length - (len(widths) + 1) * self.thickness)) / (len(widths) * 2)
else:
slot_offset = 0
slot_height = (self.settings.height - 2 * self.thickness) * self.settings.hole_height / 100
slot_x = self.thickness + slot_offset
for w in widths:
if sum(widths) > 100:
slotwidth = w / sum(widths) * (length - (len(widths) + 1) * self.thickness)
else:
slotwidth = w / 100 * (length - (len(widths) + 1) * self.thickness)
slot_x += slotwidth / 2
with self.saved_context():
self.moveTo((self.settings.height / 2) + extra_height, slot_x, 0)
self.rectangularHole(0, 0, slot_height, slotwidth, slot_height / 2, True, True)
slot_x += slotwidth / 2 + slot_offset + self.thickness + slot_offset
self.edge(self.settings.height - r + extra_height, tabs=1)
self.corner(90, r, tabs=1)
self.edge(length - 2 * r, tabs=1)
self.corner(90, r, tabs=1)
self.edge(self.settings.height - r + extra_height, tabs=1)
if self.settings.outset:
self.polyline(0, 90, self.settings.outset, -180)
else:
self.corner(-90)
[docs]
def margin(self) -> float:
return self.settings.height
[docs]
class HandleHoleEdge(HandleEdge):
"""Extends an 'edge' by adding a rounded bumpout with optional holes and holes for parallel finger joint"""
description = "Handle with holes for parallel finger joint"
char = "Y"
extra_height = 1.0
def __call__(self, length, **kw):
self.fingerHolesAt(0, -0.5 * self.settings.thickness, length, 0)
super().__call__(length, **kw)
[docs]
def margin(self) -> float:
return self.settings.height + self.extra_height * self.settings.thickness